Compare commits

...

49 Commits

Author SHA1 Message Date
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
Daniel Valdivia
c49a7bbe3c Add details on Policy for Service Account (#132) 2020-05-21 12:11:16 -07:00
Alex
3bb317535c Added path to bucket details in acls list (#131) 2020-05-20 21:59:03 -05:00
Alex
989e6f3471 Added Service Accounts page to settings (#128)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-19 13:41:46 -07:00
Lenin Alevski
35d575e7ac Allow sign-in users without policy (#129) 2020-05-19 15:26:53 -05:00
Harshavardhana
92a8aab07d upgrade linter and cleanup makefile (#126) 2020-05-18 21:55:54 -07:00
César Nieto
c5b2419191 Fix websocket apis (#127)
Remove ping check and instead use a context that will be canceled
if it the client sends a close message or an error occurs on reading.
The context will be used to cancel all functions using it.
2020-05-18 21:21:02 -07:00
Lenin Alevski
732e0ef683 ACL for mcs (#123)
This PR sets the initial version of the ACL for mcs, the idea behind
this is to start using the principle of least privileges when assigning
policies to users when creating users through mcs, currently mcsAdmin policy uses admin:*
and s3:* and by default a user with that policy will have access to everything, if want to limit
that we can create a policy with least privileges.

We need to start validating explicitly if users has acccess to an
specific endpoint based on IAM policy actions.

In this first version every endpoint (you can see it as a page to),
defines a set of well defined admin/s3 actions to work properly, ie:

```
// corresponds to /groups endpoint used by the groups page
var groupsActionSet = iampolicy.NewActionSet(
    iampolicy.ListGroupsAdminAction,
    iampolicy.AddUserToGroupAdminAction,
    //iampolicy.GetGroupAdminAction,
    iampolicy.EnableGroupAdminAction,
    iampolicy.DisableGroupAdminAction,
)

// corresponds to /policies endpoint used by the policies page
var iamPoliciesActionSet = iampolicy.NewActionSet(
    iampolicy.GetPolicyAdminAction,
    iampolicy.DeletePolicyAdminAction,
    iampolicy.CreatePolicyAdminAction,
    iampolicy.AttachPolicyAdminAction,
    iampolicy.ListUserPoliciesAdminAction,
)
```
With that said, for this initial version, now the sessions endpoint will
return a list of authorized pages to be render on the UI, on subsequent
prs we will add this verification of authorization via a server
middleware.
2020-05-18 18:03:06 -07:00
César Nieto
e8491d80cb Add size info to bucket list api (#122)
Using madmin.AccountUsageInfo since that api
includes size already.
Also includes integration with UI.
2020-05-18 13:36:18 -07:00
Daniel Valdivia
35c3b53a23 Dashboard Loading + Prettier files (#124) 2020-05-15 14:41:27 -07:00
César Nieto
6fef30f29d Add Watch api and UI integration (#120)
Uses a similar approach as Trace and Console Logs by using
websockets. It also includes the integration with the UI which
needs 3 input fields that are sent as query parameters.
2020-05-15 14:24:29 -07:00
Alex
acf480fd25 Changed help icon position next to label (#119) 2020-05-12 19:54:24 -05:00
César Nieto
8bbc4f0192 remove color dependency and any non ascii characters (#118) 2020-05-12 17:20:58 -07:00
Alex
48e7991f11 Fixed label size & added text alignment (#112)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-12 14:34:48 -07:00
Alex
5e9b0652b0 Changed modal forms to contain scrollbars (#113)
Changed modal forms to contain scrollbars where the inputs are, so this way the titles and buttons get always visible.

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-05-12 13:41:09 -05:00
Lenin Alevski
438211199d LDAP authentication support for MCS (#114)
This PR adds ldap authentication support for mcs based on
https://github.com/minio/minio/blob/master/docs/sts/ldap.md

How to test:

```
$ docker run --rm -p 389:389 -p 636:636 --name my-openldap-container
--detach osixia/openldap:1.3.0
```

Run the `billy.ldif` file using `ldapadd` command to create a new user
and assign it to a group.

```
$ cat > billy.ldif << EOF
dn: uid=billy,dc=example,dc=org
uid: billy
cn: billy
sn: 3
objectClass: top
objectClass: posixAccount
objectClass: inetOrgPerson
loginShell: /bin/bash
homeDirectory: /home/billy
uidNumber: 14583102
gidNumber: 14564100
userPassword: {SSHA}j3lBh1Seqe4rqF1+NuWmjhvtAni1JC5A
mail: billy@example.org
gecos: Billy User
dn: ou=groups,dc=example,dc=org
objectclass:organizationalunit
ou: groups
description: generic groups branch
of s3::*)
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
objectClass: top
objectClass: posixGroup
gidNumber: 678
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
changetype: modify
add: memberuid
memberuid: billy
EOF

$ docker cp billy.ldif
my-openldap-container:/container/service/slapd/assets/test/billy.ldif
$ docker exec my-openldap-container ldapadd -x -D
"cn=admin,dc=example,dc=org" -w admin -f
/container/service/slapd/assets/test/billy.ldif -H ldap://localhost -ZZ
```

Query the ldap server to check the user billy was created correctly and
got assigned to the mcsAdmin group, you should get a list
containing ldap users and groups.

```
$ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b
dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
```

Query the ldap server again, this time filtering only for the user
`billy`, you should see only 1 record.

```
$ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b
uid=billy,dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
```

Change the password for user billy

Set the new password for `billy` to `minio123` and enter `admin` as the
default `LDAP Password`

```
$ docker exec -it my-openldap-container /bin/bash
ldappasswd -H ldap://localhost -x -D "cn=admin,dc=example,dc=org" -W
-S "uid=billy,dc=example,dc=org"
New password:
Re-enter new password:
Enter LDAP Password:
```

Add the mcsAdmin policy to user billy on MinIO

```
$ cat > mcsAdmin.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "admin:*"
      ],
      "Effect": "Allow",
      "Sid": ""
    },
    {
      "Action": [
        "s3:*"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::*"
      ],
      "Sid": ""
    }
  ]
}
EOF
$ mc admin policy add myminio mcsAdmin mcsAdmin.json
$ mc admin policy set myminio mcsAdmin user=billy
```

Run MinIO

```
export MINIO_ACCESS_KEY=minio
export MINIO_SECRET_KEY=minio123
export MINIO_IDENTITY_LDAP_SERVER_ADDR='localhost:389'
export MINIO_IDENTITY_LDAP_USERNAME_FORMAT='uid=%s,dc=example,dc=org'
export
MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER='(|(objectclass=posixAccount)(uid=%s))'
export MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY=on
export MINIO_IDENTITY_LDAP_SERVER_INSECURE=on
./minio server ~/Data
```

Run MCS

```
export MCS_ACCESS_KEY=minio
export MCS_SECRET_KEY=minio123
...
export MCS_LDAP_ENABLED=on
./mcs server
```
2020-05-12 10:26:38 -07:00
Alex
0f77a32656 Fixed delete bucket event functionality (#109)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-09 10:16:15 -07:00
Daniel Valdivia
9f3e99ede8 Fix UI Metadata (#108) 2020-05-08 19:38:51 -07:00
Lenin Alevski
a8c07c0969 Connect MCS with Minio insecure TLS/Custom CAs (#102)
This PR adds support to connect MCS to minio instances running TLS with
self-signed certificates or  certificates signed by custom
Certificate Authorities

```
export MCS_MINIO_SERVER_TLS_ROOT_CAS=file1,file2,file3
```

Note: TLS Skip Verification is not supported unless there's a clear need
for it
2020-05-08 17:11:47 -07:00
Daniel Valdivia
cf8472b04c wss for websockets on tls and single page application behavior (#107) 2020-05-08 16:36:08 -07:00
Alex
317a7ebbd3 Fixed error messages in mcs (#105)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-08 12:31:09 -07:00
Alex
63f4150232 Added pagination to users page (#100)
Added pagination to users page for mcs, this resolves #96

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-06 21:47:41 -07:00
César Nieto
3bfc2556fc authenticate websocket apis using sts (#97) 2020-05-06 16:47:49 -05:00
César Nieto
511cc47d2b Add console logs api and integrate it with UI (#90)
Uses same behavior as the Trace feature using websockets.
For displaying it on the UI it needed to handle colors
since the log message comes with unicode colors embbeded
on the message.
Also a special case when an error log comes needed to be handled
to show all sources of the error.
2020-05-05 15:12:04 -07:00
Alex
9660650f41 Settings forms connection (#95)
Connected the forms to backend to send & receive the information stored in MinIO settings

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-05 11:07:06 -07:00
Lenin Alevski
9ac754d4de MCS use the correct region to authenticate users (#94)
Previous mcs was authenticating all the users agains <empty> region,
this was a problem when an admin configure a different region via the
configuration page on mcs, now before authenticating a user via
credentials or idp mcs will ask minio what's the current region and try
to authenticate using that that information.

- Login to mcs
- Go to the configuration page and change the region, ie: us-west-1
- Logout from mcs
- Login to mcs again, you should not get any error
2020-05-04 18:18:04 -05:00
César Nieto
646318e1f6 Add list and delete service accounts api (#91) 2020-05-04 15:48:38 -07:00
César Nieto
beb1ac7d04 add CREDITS file (#93) 2020-05-04 15:41:16 -07:00
César Nieto
f3a9311374 add comments to exported functions (#92) 2020-05-04 15:28:49 -07:00
168 changed files with 39519 additions and 5186 deletions

View File

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

View File

@@ -14,25 +14,23 @@ 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 }}
uses: actions/setup-go@v1
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Build on ${{ matrix.os }}
env:
GO111MODULE: on
GOOS: linux
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0
$(go env GOPATH)/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
go mod vendor
go test -v -race ./...
make verifiers
make test
make mcs

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

@@ -1,4 +1,7 @@
linters-settings:
golint:
min-confidence: 0
misspell:
locale: US
@@ -14,4 +17,8 @@ linters:
- gosimple
- deadcode
- unparam
- unused
- structcheck
service:
golangci-lint-version: 1.21.0 # use the fixed version to not introduce new linters unexpectedly

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:

23872
CREDITS Normal file

File diff suppressed because it is too large Load Diff

127
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,127 @@
# LDAP authentication with MCS
## Setup
Run openLDAP with docker.
```
$ docker run --rm -p 389:389 -p 636:636 --name my-openldap-container --detach osixia/openldap:1.3.0
```
Run the `billy.ldif` file using `ldapadd` command to create a new user and assign it to a group.
```
$ cat > billy.ldif << EOF
# LDIF fragment to create group branch under root
dn: uid=billy,dc=example,dc=org
uid: billy
cn: billy
sn: 3
objectClass: top
objectClass: posixAccount
objectClass: inetOrgPerson
loginShell: /bin/bash
homeDirectory: /home/billy
uidNumber: 14583102
gidNumber: 14564100
userPassword: {SSHA}j3lBh1Seqe4rqF1+NuWmjhvtAni1JC5A
mail: billy@example.org
gecos: Billy User
# Create base group
dn: ou=groups,dc=example,dc=org
objectclass:organizationalunit
ou: groups
description: generic groups branch
# create mcsAdmin group (this already exists on minio and have a policy of s3::*)
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
objectClass: top
objectClass: posixGroup
gidNumber: 678
# Assing group to new user
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
changetype: modify
add: memberuid
memberuid: billy
EOF
$ docker cp billy.ldif my-openldap-container:/container/service/slapd/assets/test/billy.ldif
$ docker exec my-openldap-container ldapadd -x -D "cn=admin,dc=example,dc=org" -w admin -f /container/service/slapd/assets/test/billy.ldif -H ldap://localhost -ZZ
```
Query the ldap server to check the user billy was created correctly and got assigned to the mcsAdmin group, you should get a list
containing ldap users and groups.
```
$ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
```
Query the ldap server again, this time filtering only for the user `billy`, you should see only 1 record.
```
$ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b uid=billy,dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
```
### Change the password for user billy
Set the new password for `billy` to `minio123` and enter `admin` as the default `LDAP Password`
```
$ docker exec -it my-openldap-container /bin/bash
# ldappasswd -H ldap://localhost -x -D "cn=admin,dc=example,dc=org" -W -S "uid=billy,dc=example,dc=org"
New password:
Re-enter new password:
Enter LDAP Password:
```
### Add the mcsAdmin policy to user billy on MinIO
```
$ cat > mcsAdmin.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"admin:*"
],
"Effect": "Allow",
"Sid": ""
},
{
"Action": [
"s3:*"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*"
],
"Sid": ""
}
]
}
EOF
$ mc admin policy add myminio mcsAdmin mcsAdmin.json
$ mc admin policy set myminio mcsAdmin user=billy
```
## Run MinIO
```
export MINIO_ACCESS_KEY=minio
export MINIO_SECRET_KEY=minio123
export MINIO_IDENTITY_LDAP_SERVER_ADDR='localhost:389'
export MINIO_IDENTITY_LDAP_USERNAME_FORMAT='uid=%s,dc=example,dc=org'
export MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER='(|(objectclass=posixAccount)(uid=%s))'
export MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY=on
export MINIO_IDENTITY_LDAP_SERVER_INSECURE=on
./minio server ~/Data
```
## Run MCS
```
export MCS_ACCESS_KEY=minio
export MCS_SECRET_KEY=minio123
...
export MCS_LDAP_ENABLED=on
./mcs server
```

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

@@ -6,7 +6,23 @@ default: mcs
.PHONY: mcs
mcs:
@echo "Building mcs binary to './mcs'"
@(CGO_ENABLED=0 go build -trimpath --tags=kqueue --ldflags "-s -w" -o mcs ./cmd/mcs)
@(GO111MODULE=on CGO_ENABLED=0 go build -trimpath --tags=kqueue --ldflags "-s -w" -o mcs ./cmd/mcs)
getdeps:
@mkdir -p ${GOPATH}/bin
@which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.27.0)
verifiers: getdeps fmt lint
fmt:
@echo "Running $@ check"
@GO111MODULE=on gofmt -d cmd/
@GO111MODULE=on gofmt -d pkg/
lint:
@echo "Running $@ check"
@GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
install: mcs
@echo "Installing mcs binary to '$(GOPATH)/bin/mcs'"
@@ -21,11 +37,11 @@ assets:
@(cd portal-ui; yarn install; make build-static; cd ..)
test:
@(go test -race -v github.com/minio/mcs/restapi/...)
@(go test -race -v github.com/minio/mcs/pkg/auth/...)
@(GO111MODULE=on go test -race -v github.com/minio/mcs/restapi/...)
@(GO111MODULE=on go test -race -v github.com/minio/mcs/pkg/...)
coverage:
@(go test -v -coverprofile=coverage.out github.com/minio/mcs/restapi/... && go tool cover -html=coverage.out && open coverage.html)
@(GO111MODULE=on go test -v -coverprofile=coverage.out github.com/minio/mcs/restapi/... && go tool cover -html=coverage.out && open coverage.html)
clean:
@echo "Cleaning up all the generated files"

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.
@@ -14,31 +19,30 @@ $ mc admin user add myminio mcs YOURMCSSECRET
$ set -o history
```
2. Create a policy for `mcs`
2. Create a policy for `mcs` with access to everything (for testing and debugging)
```
$ cat > mcsAdmin.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"admin:*"
],
"Effect": "Allow",
"Sid": ""
},
{
"Action": [
"s3:*"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*"
],
"Sid": ""
}
]
"Version": "2012-10-17",
"Statement": [{
"Action": [
"admin:*"
],
"Effect": "Allow",
"Sid": ""
},
{
"Action": [
"s3:*"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*"
],
"Sid": ""
}
]
}
EOF
$ mc admin policy add myminio mcsAdmin mcsAdmin.json
@@ -50,6 +54,49 @@ $ mc admin policy add myminio mcsAdmin mcsAdmin.json
$ mc admin policy set myminio mcsAdmin user=mcs
```
### Note
Additionally, you can create policies to limit the privileges for `mcs` users, for example, if you want the user to only have access to dashboard, buckets, notifications and watch page, the policy should look like this:
```
{
"Version": "2012-10-17",
"Statement": [{
"Action": [
"admin:ServerInfo",
],
"Effect": "Allow",
"Sid": ""
},
{
"Action": [
"s3:ListenBucketNotification",
"s3:PutBucketNotification",
"s3:GetBucketNotification",
"s3:ListMultipartUploadParts",
"s3:ListBucketMultipartUploads",
"s3:ListBucket",
"s3:HeadBucket",
"s3:GetObject",
"s3:GetBucketLocation",
"s3:AbortMultipartUpload",
"s3:CreateBucket",
"s3:PutObject",
"s3:DeleteObject",
"s3:DeleteBucket",
"s3:PutBucketPolicy",
"s3:DeleteBucketPolicy",
"s3:GetBucketPolicy"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*"
],
"Sid": ""
}
]
}
```
## Run MCS server
To run the server:
@@ -68,6 +115,15 @@ export MCS_MINIO_SERVER=http://localhost:9000
./mcs server
```
## Connect MCS to a Minio using TLS and a self-signed certificate
```
...
export MCS_MINIO_SERVER_TLS_SKIP_VERIFICATION=on
export MCS_MINIO_SERVER=https://localhost:9000
./mcs server
```
You can verify that the apis work by doing the request on `localhost:9090/api/v1/...`
# Contribute to mcs Project

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

8
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
@@ -17,9 +17,9 @@ require (
github.com/jessevdk/go-flags v1.4.0
github.com/json-iterator/go v1.1.9
github.com/minio/cli v1.22.0
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c
github.com/minio/minio v0.0.0-20200428222040-c3c3e9087bc1
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22
github.com/minio/mc v0.0.0-20200515235434-3b479cf92ed6
github.com/minio/minio v0.0.0-20200516011754-9cac385aecdb
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.5.1

57
go.sum
View File

@@ -12,6 +12,7 @@ github.com/Azure/azure-storage-blob-go v0.8.0 h1:53qhf0Oxa0nOjgbDeeYPUeyiNmafAFE
github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
github.com/Azure/go-autorest v11.7.1+incompatible h1:M2YZIajBBVekV86x0rr1443Lc1F/Ylxb9w+5EtSyX3Q=
github.com/Azure/go-autorest v11.7.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@@ -30,8 +31,6 @@ github.com/alecthomas/participle v0.2.1 h1:4AVLj1viSGa4LG5HDXKXrm5xRx19SB/rS/skP
github.com/alecthomas/participle v0.2.1/go.mod h1:SW6HZGeZgSIpcUWX3fXpfZhuaWHnmoD5KCVaqSaNTkk=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5 h1:nWDRPCyCltiTsANwC/n3QZH7Vww33Npq9MKqlwRzI/c=
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM=
@@ -42,8 +41,6 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/aws/aws-sdk-go v1.20.21/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2 h1:M+TYzBcNIRyzPRg66ndEqUMd7oWDmhvdQmaPC6EZNwM=
github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2/go.mod h1:RDu/qcrnpEdJC/p8tx34+YBFqqX71lB7dOX9QE+ZC4M=
github.com/beevik/ntp v0.2.0 h1:sGsd+kAXzT0bfVfzJfce04g+dSRfrs+tbQW8lweuYgw=
@@ -116,6 +113,7 @@ github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap v3.0.2+incompatible h1:kD5HQcAzlQ7yrhfn+h+MSABeAy/jAJhvIJ/QDllP44g=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
@@ -241,6 +239,7 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -340,12 +339,16 @@ github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eT
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/cpuid v1.2.2 h1:1xAgYebNnsb9LKCdLOvFWtAxGU/33mjJtyOVbmUa0Us=
github.com/klauspost/cpuid v1.2.2/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.4 h1:EBfaK0SWSwk+fgk6efYFWdzl8MwRWoOO1gkmiaTXPW4=
github.com/klauspost/cpuid v1.2.4/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/readahead v1.3.1 h1:QqXNYvm+VvqYcbrRT4LojUciM0XrznFRIDrbHiJtu/0=
github.com/klauspost/readahead v1.3.1/go.mod h1:AH9juHzNH7xqdqFHrMRSHeH2Ps+vFf+kblDqzPFiLJg=
github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY=
github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
github.com/klauspost/reedsolomon v1.9.7 h1:+azeqnT4iNG9UEcWC+7utJ4xXQ9S8pSlkZor0DOArEQ=
github.com/klauspost/reedsolomon v1.9.7/go.mod h1:+8WD025Xpby8/kG5h/HDPIFhiiuGEtZOKw+5Y4drAD8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -401,16 +404,16 @@ github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2
github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc=
github.com/minio/lsync v1.0.1 h1:AVvILxA976xc27hstd1oR+X9PQG0sPSom1MNb1ImfUs=
github.com/minio/lsync v1.0.1/go.mod h1:tCFzfo0dlvdGl70IT4IAK/5Wtgb0/BrTmo/jE8pArKA=
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c h1:JLr0fYpCleodj9nGB5hfsJU2zPdnNQKqa2bYsIvPhVw=
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c/go.mod h1:l9PuOY62zT7AQJqopDjfo/T22AIBJSb2yXPVZf4RlhM=
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab h1:9hlqghJl3e3HorXa6ADWsz6ECq790t4iQs07VD9JctM=
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab/go.mod h1:v8oQPMMaTkjDwp5cOz1WCElA4Ik+X+0y4On+VMk0fis=
github.com/minio/minio v0.0.0-20200428222040-c3c3e9087bc1 h1:DQjH/653WCerOeZCp3BxAgkmRiQybHYiprbTFs+brgA=
github.com/minio/minio v0.0.0-20200428222040-c3c3e9087bc1/go.mod h1:HxnN5FYGIii8ZH6d+LH5UNOSSIonbJkYPqP6gWelVO0=
github.com/minio/mc v0.0.0-20200515235434-3b479cf92ed6 h1:2SrKe2vLDLwvnYkYrJelrzyGW8t/8HCbr9yDsw+8XSI=
github.com/minio/mc v0.0.0-20200515235434-3b479cf92ed6/go.mod h1:U3Jgk0bcSjn+QPUMisrS6nxCWOoQ6rYWSvLCB30apuU=
github.com/minio/minio v0.0.0-20200421050159-282c9f790a03/go.mod h1:zBua5AiljGs1Irdl2XEyiJjvZVCVDIG8gjozzRBcVlw=
github.com/minio/minio v0.0.0-20200516011754-9cac385aecdb h1:CQC7D3UDnUycuxhwImcVhMSLet/RbShosAnYcvMtEB8=
github.com/minio/minio v0.0.0-20200516011754-9cac385aecdb/go.mod h1:wymaytM/HELuwdz7BGZHmQ3XKq2SxPsLeGxyOCaCLiA=
github.com/minio/minio-go/v6 v6.0.53 h1:8jzpwiOzZ5Iz7/goFWqNZRICbyWYShbb5rARjrnSCNI=
github.com/minio/minio-go/v6 v6.0.53/go.mod h1:DIvC/IApeHX8q1BAMVCXSXwpmrmM+I+iBvhvztQorfI=
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22 h1:nZEve4vdUhwHBoV18zRvPDgjL6NYyDJE5QJvz3l9bRs=
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
github.com/minio/minio-go/v6 v6.0.55-0.20200425081427-89eebdef2af0/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f h1:ifHrI8+exqLi5RztIWWKS5k+Wu+W7DJisVXwNaCH2zs=
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61 h1:pUSI/WKPdd77gcuoJkSzhJ4wdS8OMDOsOu99MtpXEQA=
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61/go.mod h1:4trzEJ7N1nBTd5Tt7OCZT5SEin+WiAXpdJ/WgPkESA8=
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
@@ -555,6 +558,7 @@ github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94 h1:0ngsPmuP6XIjiFRN
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -596,6 +600,7 @@ github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
@@ -608,11 +613,19 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.11.0 h1:gSmpCfs+R47a4yQPAI4xJ0IPDLTRGXskm6UelqNXpqE=
go.uber.org/zap v1.11.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
golang.org/x/arch v0.0.0-20190909030613-46d78d1859ac/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -621,11 +634,13 @@ golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f h1:kz4KIr+xcPUsI3VMoqWfPMvtnJ6MGfiVwsWSVzphMO4=
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg=
@@ -635,6 +650,11 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -665,6 +685,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -711,9 +733,18 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190914235951-31e00f45c22e h1:nOOVVcLC+/3MeovP40q5lCiWmP1Z1DaN8yn8ngU63hw=
golang.org/x/tools v0.0.0-20190914235951-31e00f45c22e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32 h1:Xvf3ZQTm5bjXPxhI7g+dwqsCqadK1rcNtwtszuatetk=
golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM=
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
@@ -776,4 +807,6 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

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

@@ -27,22 +27,22 @@ import (
"github.com/go-openapi/swag"
)
// ServiceAccount service account
// ServiceAccountRequest service account request
//
// swagger:model serviceAccount
type ServiceAccount struct {
// swagger:model serviceAccountRequest
type ServiceAccountRequest struct {
// policy to be applied to the Service Account if any
Policy string `json:"policy,omitempty"`
}
// Validate validates this service account
func (m *ServiceAccount) Validate(formats strfmt.Registry) error {
// Validate validates this service account request
func (m *ServiceAccountRequest) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *ServiceAccount) MarshalBinary() ([]byte, error) {
func (m *ServiceAccountRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
@@ -50,8 +50,8 @@ func (m *ServiceAccount) MarshalBinary() ([]byte, error) {
}
// UnmarshalBinary interface implementation
func (m *ServiceAccount) UnmarshalBinary(b []byte) error {
var res ServiceAccount
func (m *ServiceAccountRequest) UnmarshalBinary(b []byte) error {
var res ServiceAccountRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}

View File

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

View File

@@ -36,6 +36,9 @@ import (
// swagger:model sessionResponse
type SessionResponse struct {
// pages
Pages []string `json:"pages"`
// status
// Enum: [ok]
Status string `json:"status,omitempty"`

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"
)

315
pkg/acl/endpoints.go Normal file
View File

@@ -0,0 +1,315 @@
// 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 (
iampolicy "github.com/minio/minio/pkg/iam/policy"
)
// endpoints definition
var (
configuration = "/configurations-list"
users = "/users"
groups = "/groups"
iamPolicies = "/policies"
dashboard = "/dashboard"
profiling = "/profiling"
trace = "/trace"
logs = "/logs"
watch = "/watch"
notifications = "/notification-endpoints"
buckets = "/buckets"
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/service-accounts"
tenants = "/tenants"
tenantsDetail = "/tenants/:tenantName"
heal = "/heal"
)
type ConfigurationActionSet struct {
actionTypes iampolicy.ActionSet
actions iampolicy.ActionSet
}
// configurationActionSet contains the list of admin actions required for this endpoint to work
var configurationActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ConfigUpdateAdminAction,
),
}
// logsActionSet contains the list of admin actions required for this endpoint to work
var logsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ConsoleLogAdminAction,
),
}
// dashboardActionSet contains the list of admin actions required for this endpoint to work
var dashboardActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ServerInfoAdminAction,
),
}
// groupsActionSet contains the list of admin actions required for this endpoint to work
var groupsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListGroupsAdminAction,
iampolicy.AddUserToGroupAdminAction,
//iampolicy.GetGroupAdminAction,
iampolicy.EnableGroupAdminAction,
iampolicy.DisableGroupAdminAction,
),
}
// iamPoliciesActionSet contains the list of admin actions required for this endpoint to work
var iamPoliciesActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.GetPolicyAdminAction,
iampolicy.DeletePolicyAdminAction,
iampolicy.CreatePolicyAdminAction,
iampolicy.AttachPolicyAdminAction,
iampolicy.ListUserPoliciesAdminAction,
),
}
// profilingActionSet contains the list of admin actions required for this endpoint to work
var profilingActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ProfilingAdminAction,
),
}
// traceActionSet contains the list of admin actions required for this endpoint to work
var traceActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.TraceAdminAction,
),
}
// usersActionSet contains the list of admin actions required for this endpoint to work
var usersActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListUsersAdminAction,
iampolicy.CreateUserAdminAction,
iampolicy.DeleteUserAdminAction,
iampolicy.GetUserAdminAction,
iampolicy.EnableUserAdminAction,
iampolicy.DisableUserAdminAction,
),
}
// watchActionSet contains the list of admin actions required for this endpoint to work
var watchActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListenBucketNotificationAction,
),
}
// notificationsActionSet contains the list of admin actions required for this endpoint to work
var notificationsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListenBucketNotificationAction,
iampolicy.PutBucketNotificationAction,
iampolicy.GetBucketNotificationAction,
),
}
// bucketsActionSet contains the list of admin actions required for this endpoint to work
var bucketsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllActions,
),
actions: iampolicy.NewActionSet(
// Read access to buckets
iampolicy.ListMultipartUploadPartsAction,
iampolicy.ListBucketMultipartUploadsAction,
iampolicy.ListBucketAction,
iampolicy.HeadBucketAction,
iampolicy.GetObjectAction,
iampolicy.GetBucketLocationAction,
// Write access to buckets
iampolicy.AbortMultipartUploadAction,
iampolicy.CreateBucketAction,
iampolicy.PutObjectAction,
iampolicy.DeleteObjectAction,
iampolicy.DeleteBucketAction,
// Assign bucket policies
iampolicy.PutBucketPolicyAction,
iampolicy.DeleteBucketPolicyAction,
iampolicy.GetBucketPolicyAction,
),
}
// 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,
users: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
dashboard: dashboardActionSet,
profiling: profilingActionSet,
trace: traceActionSet,
logs: logsActionSet,
watch: watchActionSet,
notifications: notificationsActionSet,
buckets: bucketsActionSet,
bucketsDetail: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
heal: healActionSet,
}
// 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:
// {
// "Version": "2012-10-17",
// "Statement": [{
// "Action": [
// "admin:ServerInfo",
// "admin:CreatePolicy",
// "admin:GetUser"
// ],
// ...
// },
// {
// "Action": [
// "s3:ListenBucketNotification",
// "s3:PutBucketNotification"
// ],
// ...
// }
// ]
// }
// Will produce an array like: ["admin:ServerInfo", "admin:CreatePolicy", "admin:GetUser", "s3:ListenBucketNotification", "s3:PutBucketNotification"]\
func GetActionsStringFromPolicy(policy *iampolicy.Policy) []string {
var actions []string
for _, statement := range policy.Statements {
// We only care about allowed actions
if statement.Effect.IsAllowed(true) {
for _, action := range statement.Actions.ToSlice() {
actions = append(actions, string(action))
}
}
}
return actions
}
// actionsStringToActionSet convert a given string array to iampolicy.ActionSet structure
// this avoids ending with duplicate actions
func actionsStringToActionSet(actions []string) iampolicy.ActionSet {
actionsSet := iampolicy.ActionSet{}
for _, action := range actions {
actionsSet.Add(iampolicy.Action(action))
}
return actionsSet
}
// 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 rangeTake {
// check if user policy matches s3:* or admin:* typesIntersection
endpointActionTypes := rules.actionTypes
typesIntersection := endpointActionTypes.Intersection(userAllowedAction)
if len(typesIntersection) == len(endpointActionTypes.ToSlice()) {
allowedEndpoints = append(allowedEndpoints, endpoint)
continue
}
// check if user policy matches explicitly defined endpoint required actions
endpointRequiredActions := rules.actions
actionsIntersection := endpointRequiredActions.Intersection(userAllowedAction)
if len(actionsIntersection) == len(endpointRequiredActions.ToSlice()) {
allowedEndpoints = append(allowedEndpoints, endpoint)
}
}
return allowedEndpoints
}

190
pkg/acl/endpoints_test.go Normal file
View File

@@ -0,0 +1,190 @@
// This file is part of MinIO Orchestrator
// 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 (
"reflect"
"testing"
iampolicy "github.com/minio/minio/pkg/iam/policy"
)
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)
}
})
}
}
func TestGetAuthorizedEndpoints(t *testing.T) {
tests := []endpoint{
{
name: "dashboard endpoint",
args: args{
[]string{"admin:ServerInfo"},
},
want: 2,
},
{
name: "policies endpoint",
args: args{
[]string{
"admin:CreatePolicy",
"admin:DeletePolicy",
"admin:GetPolicy",
"admin:AttachUserOrGroupPolicy",
"admin:ListUserPolicies",
},
},
want: 2,
},
{
name: "all admin endpoints",
args: args{
[]string{
"admin:*",
},
},
want: 11,
},
{
name: "all s3 endpoints",
args: args{
[]string{
"s3:*",
},
},
want: 4,
},
{
name: "all admin and s3 endpoints",
args: args{
[]string{
"admin:*",
"s3:*",
},
},
want: 14,
},
{
name: "no endpoints",
args: args{
[]string{},
},
want: 0,
},
}
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) {
type args struct {
policy *iampolicy.Policy
}
tests := []struct {
name string
args args
want int
}{
{
name: "parse ReadOnly policy",
args: args{
policy: &iampolicy.ReadOnly,
},
want: 2,
},
{
name: "parse WriteOnly policy",
args: args{
policy: &iampolicy.WriteOnly,
},
want: 1,
},
{
name: "parse AdminDiagnostics policy",
args: args{
policy: &iampolicy.AdminDiagnostics,
},
want: 6,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetActionsStringFromPolicy(tt.args.policy); !reflect.DeepEqual(len(got), tt.want) {
t.Errorf("GetActionsStringFromPolicy() = %v, want %v", len(got), tt.want)
}
})
}
}

View File

@@ -37,7 +37,7 @@ import (
)
var (
errAuthentication = errors.New("Authentication failed, check your access credentials")
errAuthentication = errors.New("authentication failed, check your access credentials")
errNoAuthToken = errors.New("JWT token missing")
errReadingToken = errors.New("JWT internal data is malformed")
errClaimsFormat = errors.New("encrypted jwt claims not in the right format")
@@ -57,6 +57,7 @@ type DecryptedClaims struct {
AccessKeyID string
SecretAccessKey string
SessionToken string
Actions []string
}
// JWTAuthenticate takes a jwt, decode it, extract claims and validate the signature
@@ -93,9 +94,9 @@ func JWTAuthenticate(token string) (*DecryptedClaims, error) {
// NewJWTWithClaimsForClient generates a new jwt with claims based on the provided STS credentials, first
// encrypts the claims and the sign them
func NewJWTWithClaimsForClient(credentials *credentials.Value, audience string) (string, error) {
func NewJWTWithClaimsForClient(credentials *credentials.Value, actions []string, audience string) (string, error) {
if credentials != nil {
encryptedClaims, err := encryptClaims(credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken)
encryptedClaims, err := encryptClaims(credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken, actions)
if err != nil {
return "", err
}
@@ -112,8 +113,8 @@ func NewJWTWithClaimsForClient(credentials *credentials.Value, audience string)
// encryptClaims() receives the 3 STS claims, concatenate them and encrypt them using AES-GCM
// returns a base64 encoded ciphertext
func encryptClaims(accessKeyID, secretAccessKey, sessionToken string) (string, error) {
payload := []byte(fmt.Sprintf("%s:%s:%s", accessKeyID, secretAccessKey, sessionToken))
func encryptClaims(accessKeyID, secretAccessKey, sessionToken string, actions []string) (string, error) {
payload := []byte(fmt.Sprintf("%s#%s#%s#%s", accessKeyID, secretAccessKey, sessionToken, strings.Join(actions, ",")))
ciphertext, err := encrypt(payload)
if err != nil {
return "", err
@@ -133,16 +134,18 @@ func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
log.Println(err)
return nil, errClaimsFormat
}
s := strings.Split(string(plaintext), ":")
s := strings.Split(string(plaintext), "#")
// Validate that the decrypted string has the right format "accessKeyID:secretAccessKey:sessionToken"
if len(s) != 3 {
if len(s) != 4 {
return nil, errClaimsFormat
}
accessKeyID, secretAccessKey, sessionToken := s[0], s[1], s[2]
accessKeyID, secretAccessKey, sessionToken, actions := s[0], s[1], s[2], s[3]
actionsList := strings.Split(actions, ",")
return &DecryptedClaims{
AccessKeyID: accessKeyID,
SecretAccessKey: secretAccessKey,
SessionToken: sessionToken,
Actions: actionsList,
}, nil
}

View File

@@ -37,14 +37,14 @@ func TestNewJWTWithClaimsForClient(t *testing.T) {
funcAssert := assert.New(t)
// Test-1 : NewJWTWithClaimsForClient() is generated correctly without errors
function := "NewJWTWithClaimsForClient()"
jwt, err := NewJWTWithClaimsForClient(creds, audience)
jwt, err := NewJWTWithClaimsForClient(creds, []string{""}, audience)
if err != nil || jwt == "" {
t.Errorf("Failed on %s:, error occurred: %s", function, err)
}
// saving jwt for future tests
goodToken = jwt
// Test-2 : NewJWTWithClaimsForClient() throws error because of empty credentials
if _, err = NewJWTWithClaimsForClient(nil, audience); err != nil {
if _, err = NewJWTWithClaimsForClient(nil, []string{""}, audience); err != nil {
funcAssert.Equal("provided credentials are empty", err.Error())
}
}
@@ -63,7 +63,7 @@ func TestJWTAuthenticate(t *testing.T) {
}
// Test-2 : JWTAuthenticate() return an error because of a tampered jwt
if _, err := JWTAuthenticate(badToken); err != nil {
funcAssert.Equal("Authentication failed, check your access credentials", err.Error())
funcAssert.Equal("authentication failed, check your access credentials", err.Error())
}
// Test-3 : JWTAuthenticate() return an error because of an empty jwt
if _, err := JWTAuthenticate(""); err != nil {

39
pkg/auth/ldap.go Normal file
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/>.
package auth
import (
"errors"
"log"
"github.com/minio/minio-go/v6/pkg/credentials"
)
var (
errInvalidCredentials = errors.New("invalid Credentials")
)
// GetMcsCredentialsFromLDAP authenticates the user against MinIO when the LDAP integration is enabled
// if the authentication succeed *credentials.Credentials object is returned and we continue with the normal STSAssumeRole flow
func GetMcsCredentialsFromLDAP(endpoint, ldapUser, ldapPassword string) (*credentials.Credentials, error) {
creds, err := credentials.NewLDAPIdentity(endpoint, ldapUser, ldapPassword)
if err != nil {
log.Println("LDAP authentication error: ", err)
return nil, errInvalidCredentials
}
return creds, nil
}

27
pkg/auth/ldap/config.go Normal file
View File

@@ -0,0 +1,27 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package ldap
import (
"strings"
"github.com/minio/minio/pkg/env"
)
func GetLDAPEnabled() bool {
return strings.ToLower(env.Get(MCSLDAPEnabled, "off")) == "on"
}

22
pkg/auth/ldap/const.go Normal file
View File

@@ -0,0 +1,22 @@
// 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 ldap
const (
// const for ldap configuration
MCSLDAPEnabled = "MCS_LDAP_ENABLED"
)

View File

@@ -22,13 +22,14 @@ import (
"strings"
"github.com/go-openapi/errors"
"github.com/minio/mcs/pkg/auth"
"github.com/go-openapi/swag"
)
// Authenticate validates websocket header and returns mcs jwt claims
// GetTokenFromRequest returns a token from a http Request
// either defined on a cookie `token` or on Authorization header.
//
// Authorization Header needs to be like "Authorization Bearer <jwt_token>"
func Authenticate(r *http.Request) (*auth.DecryptedClaims, error) {
func GetTokenFromRequest(r *http.Request) (*string, error) {
// Get Auth token
var reqToken string
@@ -46,11 +47,5 @@ func Authenticate(r *http.Request) (*auth.DecryptedClaims, error) {
} else {
reqToken = strings.TrimSpace(tokenCookie.Value)
}
// Perform authentication before upgrading to a Websocket Connection
claims, err := auth.JWTAuthenticate(reqToken)
if err != nil {
return nil, errors.New(http.StatusUnauthorized, err.Error())
}
return claims, nil
return swag.String(reqToken), nil
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,8 @@
"@types/superagent": "^4.1.4",
"@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",
@@ -27,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

@@ -6,7 +6,7 @@
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Acme cloud storage"
content="MinIO Console"
/>
<!--
manifest.json provides metadata used when your web app is installed on a

View File

@@ -35,7 +35,7 @@ const isLoggedIn = () => {
};
const mapState = (state: AppState) => ({
loggedIn: state.system.loggedIn
loggedIn: state.system.loggedIn,
});
const connector = connect(mapState, { userLoggedIn });

View File

@@ -16,6 +16,7 @@
import storage from "local-storage-fallback";
import request from "superagent";
import get from "lodash/get";
export class API {
invoke(method: string, url: string, data?: object) {
@@ -23,8 +24,8 @@ export class API {
return request(method, url)
.set("Authorization", `Bearer ${token}`)
.send(data)
.then(res => res.body)
.catch(err => {
.then((res) => res.body)
.catch((err) => {
// if we get unauthorized, kick out the user
if (err.status === 401) {
storage.removeItem("token");
@@ -36,10 +37,16 @@ export class API {
onError(err: any) {
if (err.status) {
const errMessage: string =
(err.response.body && err.response.body.error) ||
err.status.toString(10);
return Promise.reject(errMessage);
const errMessage = get(
err.response,
"body.message",
err.status.toString()
);
const throwMessage =
errMessage.charAt(0).toUpperCase() + errMessage.slice(1);
return Promise.reject(throwMessage);
} else {
return Promise.reject("Unknown error");
}

View File

@@ -14,7 +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/>.
export const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
export const units = [
"B",
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
];
export const k8sUnits = ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"];
export const niceBytes = (x: string) => {
let l = 0,
n = parseInt(x, 10) || 0;
@@ -38,3 +49,46 @@ export const setCookie = (name: string, val: string) => {
document.cookie =
name + "=" + value + "; expires=" + date.toUTCString() + "; path=/";
};
// timeFromdate gets time string from date input
export const timeFromDate = (d: Date) => {
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;
let s = d.getSeconds() < 10 ? `0${d.getSeconds()}` : `${d.getSeconds()}`;
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

@@ -19,6 +19,7 @@ import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import api from "../../../../common/api";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
@@ -26,11 +27,12 @@ import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBo
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
buttonContainer: {
textAlign: "right"
}
textAlign: "right",
},
...modalBasic,
});
interface IAddBucketProps {
@@ -49,7 +51,7 @@ class AddBucket extends React.Component<IAddBucketProps, IAddBucketState> {
state: IAddBucketState = {
addLoading: false,
addError: "",
bucketName: ""
bucketName: "",
};
addRecord(event: React.FormEvent) {
@@ -61,23 +63,23 @@ class AddBucket extends React.Component<IAddBucketProps, IAddBucketState> {
this.setState({ addLoading: true }, () => {
api
.invoke("POST", "/api/v1/buckets", {
name: bucketName
name: bucketName,
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
});
@@ -106,31 +108,29 @@ class AddBucket extends React.Component<IAddBucketProps, IAddBucketState> {
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
<InputBoxWrapper
id="bucket-name"
name="bucket-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ bucketName: e.target.value });
}}
label="Bucket Name"
value={bucketName}
/>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="bucket-name"
name="bucket-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ bucketName: e.target.value });
}}
label="Bucket Name"
value={bucketName}
/>
</Grid>
<Grid item xs={12}>
<br />
<br />
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button

View File

@@ -30,49 +30,50 @@ import AddBucket from "./AddBucket";
import DeleteBucket from "./DeleteBucket";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../../../icons";
import { niceBytes } from "../../../../common/utils";
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 IListBucketsProps {
@@ -108,7 +109,7 @@ class ListBuckets extends React.Component<
rowsPerPage: 10,
deleteOpen: false,
selectedBucket: "",
filterBuckets: ""
filterBuckets: "",
};
fetchRecords() {
@@ -122,7 +123,7 @@ class ListBuckets extends React.Component<
loading: false,
records: res.buckets || [],
totalRecords: !res.buckets ? 0 : res.total,
error: ""
error: "",
});
// if we get 0 results, and page > 0 , go down 1 page
if (
@@ -174,7 +175,7 @@ class ListBuckets extends React.Component<
rowsPerPage,
deleteOpen,
selectedBucket,
filterBuckets
filterBuckets,
} = this.state;
const offset = page * rowsPerPage;
@@ -196,7 +197,7 @@ class ListBuckets extends React.Component<
const tableActions = [
{ type: "view", to: `/buckets`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteBucket, sendOnlyId: true }
{ type: "delete", onClick: confirmDeleteBucket, sendOnlyId: true },
];
const displayParsedDate = (date: string) => {
@@ -249,9 +250,9 @@ class ListBuckets extends React.Component<
className={classes.searchField}
id="search-resource"
label=""
onChange={val => {
onChange={(val) => {
this.setState({
filterBuckets: val.target.value
filterBuckets: val.target.value,
});
}}
InputProps={{
@@ -260,7 +261,7 @@ class ListBuckets extends React.Component<
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
/>
<Button
@@ -269,7 +270,7 @@ class ListBuckets extends React.Component<
startIcon={<CreateIcon />}
onClick={() => {
this.setState({
addScreenOpen: true
addScreenOpen: true,
});
}}
>
@@ -287,8 +288,13 @@ class ListBuckets extends React.Component<
{
label: "Creation Date",
elementKey: "creation_date",
renderFunction: displayParsedDate
}
renderFunction: displayParsedDate,
},
{
label: "Size",
elementKey: "size",
renderFunction: niceBytes,
},
]}
isLoading={loading}
records={filteredRecords}
@@ -302,11 +308,11 @@ class ListBuckets extends React.Component<
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>

View File

@@ -27,6 +27,7 @@ import TableBody from "@material-ui/core/TableBody";
import Checkbox from "@material-ui/core/Checkbox";
import Table from "@material-ui/core/Table";
import { ArnList } from "../types";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
@@ -34,19 +35,20 @@ import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapp
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
buttonContainer: {
textAlign: "right"
}
textAlign: "right",
},
...modalBasic,
});
interface IAddEventProps {
@@ -74,7 +76,7 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
suffix: "",
arn: "",
selectedEvents: [],
arnList: []
arnList: [],
};
addRecord(event: React.FormEvent) {
@@ -91,25 +93,25 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
arn: arn,
events: selectedEvents,
prefix: prefix,
suffix: suffix
suffix: suffix,
},
ignoreExisting: true
ignoreExisting: true,
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
});
@@ -127,7 +129,7 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
this.setState({
addLoading: false,
arnList: arns,
addError: ""
addError: "",
});
})
.catch((err: any) => {
@@ -149,13 +151,13 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
selectedEvents,
arnList,
prefix,
suffix
suffix,
} = this.state;
const events = [
{ label: "PUT - Object Uploaded", value: "put" },
{ label: "GET - Object accessed", value: "get" },
{ label: "DELETE - Object Deleted", value: "delete" }
{ label: "DELETE - Object Deleted", value: "delete" },
];
const handleClick = (
@@ -181,9 +183,9 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
this.setState({ selectedEvents: newSelected });
};
const arnValues = arnList.map(arnConstant => ({
const arnValues = arnList.map((arnConstant) => ({
label: arnConstant,
value: arnConstant
value: arnConstant,
}));
return (
@@ -204,89 +206,91 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
<SelectWrapper
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ arn: e.target.value as string });
}}
id="select-access-policy"
name="select-access-policy"
label={"ARN"}
value={arn}
options={arnValues}
/>
</Grid>
)}
<Grid item xs={12}>
<SelectWrapper
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ arn: e.target.value as string });
}}
id="select-access-policy"
name="select-access-policy"
label={"ARN"}
value={arn}
options={arnValues}
/>
</Grid>
<Grid item xs={12}>
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Event</TableCell>
</TableRow>
</TableHead>
<TableBody>
{events.map(row => (
<TableRow
key={`group-${row.value}`}
onClick={event => handleClick(event, row.value)}
>
<TableCell padding="checkbox">
<Checkbox
value={row.value}
color="primary"
inputProps={{
"aria-label": "secondary checkbox"
}}
onChange={event => handleClick(event, row.value)}
checked={selectedEvents.includes(row.value)}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.label}
</TableCell>
<Grid item xs={12}>
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Event</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="prefix-input"
name="prefix-input"
label="Prefix"
value={prefix}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ prefix: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="suffix-input"
name="suffix-input"
label="Suffix"
value={suffix}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ suffix: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</TableHead>
<TableBody>
{events.map((row) => (
<TableRow
key={`group-${row.value}`}
onClick={(event) => handleClick(event, row.value)}
>
<TableCell padding="checkbox">
<Checkbox
value={row.value}
color="primary"
inputProps={{
"aria-label": "secondary checkbox",
}}
onChange={(event) => handleClick(event, row.value)}
checked={selectedEvents.includes(row.value)}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.label}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="prefix-input"
name="prefix-input"
label="Prefix"
value={prefix}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ prefix: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="suffix-input"
name="suffix-input"
label="Suffix"
value={suffix}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ suffix: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button

View File

@@ -14,8 +14,9 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import get from "lodash/get";
import {
Button,
Dialog,
@@ -23,7 +24,7 @@ import {
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress
LinearProgress,
} from "@material-ui/core";
import api from "../../../../common/api";
import { BucketEvent, BucketList } from "../types";
@@ -32,8 +33,8 @@ import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
});
interface IDeleteEventProps {
@@ -55,7 +56,7 @@ class DeleteEvent extends React.Component<
> {
state: IDeleteEventState = {
deleteLoading: false,
deleteError: ""
deleteError: "",
};
removeRecord() {
@@ -69,29 +70,34 @@ class DeleteEvent extends React.Component<
}
this.setState({ deleteLoading: true }, () => {
const events = get(bucketEvent, "events", []);
const prefix = get(bucketEvent, "prefix", "");
const suffix = get(bucketEvent, "suffix", "");
api
.invoke(
"DELETE",
`/api/v1/buckets/${selectedBucket}/events/${bucketEvent.id}`,
`/api/v1/buckets/${selectedBucket}/events/${bucketEvent.arn}`,
{
name: selectedBucket
events,
prefix,
suffix,
}
)
.then((res: BucketList) => {
this.setState(
{
deleteLoading: false,
deleteError: ""
deleteError: "",
},
() => {
this.props.closeDeleteModalAndRefresh(true);
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
deleteLoading: false,
deleteError: err
deleteError: err,
});
});
});

View File

@@ -18,6 +18,7 @@ import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import api from "../../../../common/api";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
@@ -25,14 +26,16 @@ import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapp
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
...modalBasic,
});
interface ISetAccessPolicyProps {
classes: any;
open: boolean;
bucketName: string;
actualPolicy: string;
closeModalAndRefresh: () => void;
}
@@ -49,7 +52,7 @@ class SetAccessPolicy extends React.Component<
state: ISetAccessPolicyState = {
addLoading: false,
addError: "",
accessPolicy: ""
accessPolicy: "",
};
addRecord(event: React.FormEvent) {
@@ -62,30 +65,36 @@ class SetAccessPolicy extends React.Component<
this.setState({ addLoading: true }, () => {
api
.invoke("PUT", `/api/v1/buckets/${bucketName}/set-policy`, {
access: accessPolicy
access: accessPolicy,
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
});
}
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
@@ -105,34 +114,33 @@ class SetAccessPolicy extends React.Component<
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
<SelectWrapper
value={accessPolicy}
label="Access Policy"
id="select-access-policy"
name="select-access-policy"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ accessPolicy: e.target.value as string });
}}
options={[
{ value: "PRIVATE", label: "Private" },
{ value: "PUBLIC", label: "Public" },
]}
/>
</Grid>
)}
<Grid item xs={12}>
<SelectWrapper
value={accessPolicy}
label="Access Policy"
id="select-access-policy"
name="select-access-policy"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ accessPolicy: e.target.value as string });
}}
options={[
{ value: "PRIVATE", label: "Private" },
{ value: "PUBLIC", label: "Public" }
]}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button

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,54 +32,91 @@ 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({
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",
},
noRecords: {
lineHeight: "24px",
textAlign: "center",
padding: "20px"
}
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,20 +150,25 @@ 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,
addScreenOpen: false,
deleteOpen: false,
selectedBucket: "",
selectedEvent: null
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,10 +179,10 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
const total = get(res, "total", 0);
this.setState({
loading: false,
loadingEvents: false,
records: events || [],
totalRecords: total,
error: ""
error: "",
});
// if we get 0 results, and page > 0 , go down 1 page
if ((!events || res.events.length === 0) && page > 0) {
@@ -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
selectedEvent,
bucketSize,
loadingSize,
} = this.state;
const offset = page * rowsPerPage;
@@ -228,21 +315,27 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
return (
<React.Fragment>
<AddEvent
open={addScreenOpen}
selectedBucket={bucketName}
closeModalAndRefresh={() => {
this.setState({ addScreenOpen: false });
this.fetchEvents();
}}
/>
<SetAccessPolicy
bucketName={bucketName}
open={setAccessPolicyScreenOpen}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
/>
{addScreenOpen && (
<AddEvent
open={addScreenOpen}
selectedBucket={bucketName}
closeModalAndRefresh={() => {
this.setState({ addScreenOpen: false });
this.fetchEvents();
}}
/>
)}
{setAccessPolicyScreenOpen && (
<SetAccessPolicy
bucketName={bucketName}
open={setAccessPolicyScreenOpen}
actualPolicy={accessPolicy}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">
@@ -253,39 +346,78 @@ 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
addScreenOpen: true,
});
}}
>
@@ -303,12 +435,12 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
{
label: "Events",
elementKey: "events",
renderFunction: eventsDisplay
renderFunction: eventsDisplay,
},
{ label: "Prefix", elementKey: "prefix" },
{ label: "Suffix", elementKey: "suffix" }
{ label: "Suffix", elementKey: "suffix" },
]}
isLoading={loading}
isLoading={loadingEvents}
records={filteredRecords}
entityName="Events"
idField="id"
@@ -320,11 +452,11 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>

View File

@@ -13,19 +13,13 @@
//
// 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,
useCallback
} from "react";
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 InputBoxWrapper from "../InputBoxWrapper/InputBoxWrapper";
import { InputLabel, Tooltip } from "@material-ui/core";
import { fieldBasic } from "../common/styleLibrary";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface ICSVMultiSelector {
@@ -40,9 +34,10 @@ interface ICSVMultiSelector {
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
marginBottom: 10
width: 116,
},
inputContainer: {
height: 150,
@@ -50,11 +45,10 @@ const styles = (theme: Theme) =>
padding: 15,
position: "relative",
border: "1px solid #c4c4c4",
marginBottom: 10
},
labelContainer: {
display: "flex"
}
display: "flex",
},
});
const CSVMultiSelector = ({
@@ -63,26 +57,30 @@ const CSVMultiSelector = ({
label,
tooltip = "",
onChange,
classes
classes,
}: ICSVMultiSelector) => {
const [currentElements, setCurrentElements] = useState<string[]>([""]);
const bottomList = createRef<HTMLDivElement>();
// Use effect to get the initial values from props
useCallback(() => {
if (currentElements.length === 1 && currentElements[0] === "") {
const elementsSplitted = elements.split(",");
if (elementsSplitted[elementsSplitted.length - 1].trim() !== "") {
elementsSplitted.push("");
}
setCurrentElements(elementsSplitted);
useEffect(() => {
if (
currentElements.length === 1 &&
currentElements[0] === "" &&
elements &&
elements !== ""
) {
const elementsSplit = elements.split(",");
elementsSplit.push("");
setCurrentElements(elementsSplit);
}
}, [elements, setCurrentElements, currentElements]);
}, [elements, currentElements]);
// Use effect to send new values to onChange
useEffect(() => {
const elementsString = currentElements
.filter(element => element.trim() !== "")
.filter((element) => element.trim() !== "")
.join(",");
onChange(elementsString);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -131,18 +129,20 @@ const CSVMultiSelector = ({
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel className={classes.inputLabel}>{label}</InputLabel>
<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} className={classes.inputContainer}>
{inputs}
<div ref={bottomList} />
</Grid>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="left">
<HelpIcon />
</Tooltip>
</div>
)}
</Grid>
</React.Fragment>
);

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

@@ -19,16 +19,16 @@ import {
InputLabel,
TextField,
TextFieldProps,
Tooltip
Tooltip,
} from "@material-ui/core";
import { OutlinedInputProps } from "@material-ui/core/OutlinedInput";
import {
createStyles,
makeStyles,
Theme,
withStyles
withStyles,
} from "@material-ui/core/styles";
import { fieldBasic } from "../common/styleLibrary";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface InputBoxProps {
@@ -49,22 +49,23 @@ interface InputBoxProps {
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
textBoxContainer: {
flexGrow: 1
}
flexGrow: 1,
},
});
const inputStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
borderColor: "#393939",
borderRadius: 0
borderRadius: 0,
},
input: {
padding: "11px 20px",
color: "#393939",
fontSize: 14
}
fontSize: 14,
},
})
);
@@ -91,16 +92,24 @@ const InputBoxWrapper = ({
multiline = false,
tooltip = "",
index = 0,
classes
classes,
}: InputBoxProps) => {
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
{label}
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
)}
<div className={classes.textBoxContainer}>
<InputField
className={classes.boxDesign}
@@ -117,13 +126,6 @@ const InputBoxWrapper = ({
inputProps={{ "data-index": index }}
/>
</div>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="left">
<HelpIcon />
</Tooltip>
</div>
)}
</Grid>
</React.Fragment>
);

View File

@@ -23,9 +23,9 @@ import {
createStyles,
Theme,
withStyles,
makeStyles
makeStyles,
} from "@material-ui/core/styles";
import { fieldBasic } from "../common/styleLibrary";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
export interface SelectorTypes {
@@ -48,22 +48,23 @@ interface RadioGroupProps {
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
radioBoxContainer: {
flexGrow: 1
}
flexGrow: 1,
},
});
const radioStyles = makeStyles({
root: {
"&:hover": {
backgroundColor: "transparent"
}
backgroundColor: "transparent",
},
},
icon: {
borderRadius: "100%",
width: 14,
height: 14,
border: "1px solid #000"
border: "1px solid #000",
},
checkedIcon: {
borderRadius: "100%",
@@ -81,9 +82,9 @@ const radioStyles = makeStyles({
position: "absolute",
backgroundColor: "#000",
top: 2,
left: 2
}
}
left: 2,
},
},
});
const RadioButton = (props: RadioProps) => {
@@ -110,14 +111,22 @@ export const RadioGroupSelector = ({
onChange,
tooltip = "",
classes,
displayInColumn = false
displayInColumn = false,
}: RadioGroupProps) => {
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel htmlFor={id} className={classes.inputLabel}>
{label}
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
<div className={classes.radioBoxContainer}>
<RadioGroup
aria-label={id}
@@ -127,7 +136,7 @@ export const RadioGroupSelector = ({
onChange={onChange}
row={!displayInColumn}
>
{selectorOptions.map(selectorOption => {
{selectorOptions.map((selectorOption) => {
return (
<FormControlLabel
key={`rd-${name}-${selectorOption.value}`}
@@ -139,13 +148,6 @@ export const RadioGroupSelector = ({
})}
</RadioGroup>
</div>
{tooltip !== "" && (
<div>
<Tooltip title={tooltip} placement="left">
<HelpIcon />
</Tooltip>
</div>
)}
</Grid>
</React.Fragment>
);

View File

@@ -20,10 +20,12 @@ import {
InputLabel,
MenuItem,
Select,
InputBase
InputBase,
Tooltip,
} from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { fieldBasic } from "../common/styleLibrary";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface selectorTypes {
label: string;
@@ -36,6 +38,7 @@ interface SelectProps {
label: string;
id: string;
name: string;
tooltip?: string;
onChange: (
e: React.ChangeEvent<{ name?: string | undefined; value: unknown }>
) => void;
@@ -44,15 +47,20 @@ interface SelectProps {
const styles = (theme: Theme) =>
createStyles({
...fieldBasic
...fieldBasic,
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
width: 116,
},
});
const SelectStyled = withStyles((theme: Theme) =>
createStyles({
root: {
"label + &": {
marginTop: theme.spacing(3)
}
marginTop: theme.spacing(3),
},
},
input: {
borderRadius: 0,
@@ -62,12 +70,12 @@ const SelectStyled = withStyles((theme: Theme) =>
padding: "11px 20px",
border: "1px solid #c4c4c4",
"&:hover": {
borderColor: "#393939"
borderColor: "#393939",
},
"&:focus": {
backgroundColor: "#fff"
}
}
backgroundColor: "#fff",
},
},
})
)(InputBase);
@@ -78,14 +86,24 @@ const SelectWrapper = ({
onChange,
options,
label,
value
tooltip = "",
value,
}: SelectProps) => {
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel htmlFor={id} className={classes.inputLabel}>
{label}
</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}
@@ -94,7 +112,7 @@ const SelectWrapper = ({
onChange={onChange}
input={<SelectStyled />}
>
{options.map(option => (
{options.map((option) => (
<MenuItem
value={option.value}
key={`select-${name}-${option.label}`}

View File

@@ -19,17 +19,60 @@
export const fieldBasic = {
inputLabel: {
fontWeight: 500,
marginRight: 16,
minWidth: 90,
marginRight: 10,
width: 100,
fontSize: 14,
color: "#393939"
color: "#393939",
textAlign: "right" as const,
display: "flex",
textOverflow: "ellipsis",
overflow: "hidden",
justifyContent: "flex-end",
"& span": {
display: "flex",
alignItems: "center",
},
},
fieldContainer: {
display: "flex",
alignItems: "center",
marginBottom: 10
marginBottom: 10,
},
tooltipContainer: {
marginLeft: 5
}
marginLeft: 5,
display: "flex",
alignItems: "center",
},
};
export const modalBasic = {
formScrollable: {
maxHeight: "calc(100vh - 300px)" as const,
overflowY: "auto" as const,
marginBottom: 25,
},
formSlider: {
marginLeft: 0,
},
};
export const tooltipHelper = {
tooltip: {
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

@@ -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

@@ -26,11 +26,13 @@ import {
TableRow,
Paper,
Grid,
Checkbox
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
@@ -84,25 +86,20 @@ const borderColor = "#eaeaea";
const rowText = {
fontWeight: 400,
fontSize: 14,
borderColor: borderColor
};
const checkBoxBasic = {
width: 16,
height: 16,
borderRadius: 3
borderColor: borderColor,
};
const styles = (theme: Theme) =>
createStyles({
dialogContainer: {
padding: "12px 26px 22px"
padding: "12px 26px 22px",
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column",
padding: "19px 38px"
padding: "19px 38px",
minHeight: "200px",
},
minTableHeader: {
color: "#393939",
@@ -111,43 +108,42 @@ const styles = (theme: Theme) =>
fontWeight: 700,
fontSize: 14,
paddingBottom: 15,
borderColor: borderColor
}
}
borderColor: borderColor,
},
},
},
rowUnselected: {
...rowText
...rowText,
},
rowSelected: {
...rowText,
color: "#201763"
color: "#201763",
},
paginatorContainer: {
display: "flex",
justifyContent: "flex-end",
padding: "5px 38px"
padding: "5px 38px",
},
checkBoxHeader: {
"&.MuiTableCell-paddingCheckbox": {
paddingBottom: 9
}
paddingBottom: 9,
},
},
actionsContainer: {
width: 120,
borderColor: borderColor
width: 150,
borderColor: borderColor,
},
paginatorComponent: {
borderBottom: 0
},
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #d0d0d0" },
checkedIcon: {
...checkBoxBasic,
border: "1px solid #201763",
backgroundColor: "#201763"
borderBottom: 0,
},
checkBoxRow: {
borderColor: borderColor
}
borderColor: borderColor,
},
loadingBox: {
paddingTop: "100px",
paddingBottom: "100px",
},
...checkboxIcons,
});
// Function that renders Title Columns
@@ -221,12 +217,21 @@ const TableWrapper = ({
idField,
classes,
stickyHeader = false,
paginatorConfig
paginatorConfig,
}: TableWrapperProps) => {
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

@@ -17,40 +17,67 @@
import React, { useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import { IElementValue, KVField } from "./types";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import { IElementValue, KVField } from "./types";
import CSVMultiSelector from "../Common/FormComponents/CSVMultiSelector/CSVMultiSelector";
interface IConfGenericProps {
onChange: (newValue: IElementValue[]) => void;
fields: KVField[];
defaultVals?: IElementValue[];
classes: any;
}
const styles = (theme: Theme) => createStyles({});
const styles = (theme: Theme) =>
createStyles({
...modalBasic,
});
// Function to get defined values,
//we make this because the backed sometimes don't return all the keys when there is an initial configuration
export const valueDef = (
key: string,
type: string,
defaults: IElementValue[]
) => {
let defValue = type === "on|off" ? "false" : "";
if (defaults.length > 0) {
const storedConfig = defaults.find((element) => element.key === key);
if (storedConfig) {
defValue = storedConfig.value;
}
}
return defValue;
};
const ConfTargetGeneric = ({
onChange,
fields,
classes
defaultVals,
classes,
}: IConfGenericProps) => {
const [valueHolder, setValueHolder] = useState<IElementValue[]>([]);
const fieldsElements = !fields ? [] : fields;
const defValList = !defaultVals ? [] : defaultVals;
// Effect to create all the values to hold
useEffect(() => {
const values: IElementValue[] = [];
fields.forEach(field => {
fields.forEach((field) => {
const stateInsert: IElementValue = {
key: field.name,
value: field.type === "on|off" ? "false" : ""
value: valueDef(field.name, field.type, defValList),
};
values.push(stateInsert);
});
setValueHolder(values);
}, [fields]);
}, [fields, defaultVals]);
useEffect(() => {
onChange(valueHolder);
@@ -79,7 +106,7 @@ const ConfTargetGeneric = ({
}
selectorOptions={[
{ label: "On", value: "true" },
{ label: "Off", value: "false" }
{ label: "Off", value: "false" },
]}
/>
);
@@ -114,18 +141,14 @@ const ConfTargetGeneric = ({
return (
<Grid container>
{fieldsElements.map((field, item) => (
<React.Fragment key={field.name}>
<Grid item xs={12}>
{fieldDefinition(field, item)}
</Grid>
</React.Fragment>
))}
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<br />
<Grid xs={12} item className={classes.formScrollable}>
{fieldsElements.map((field, item) => (
<React.Fragment key={field.name}>
<Grid item xs={12}>
{fieldDefinition(field, item)}
</Grid>
</React.Fragment>
))}
</Grid>
</Grid>
);

View File

@@ -35,38 +35,37 @@ interface IListConfiguration {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
boxShadow: "0px 3px 6px #00000012",
},
iconText: {
lineHeight: "24px"
}
lineHeight: "24px",
},
});
const ConfigurationsList = ({ classes }: IListConfiguration) => {
const [editScreenOpen, setEditScreenOpen] = useState(false);
const [selectedConfiguration, setSelectedConfiguration] = useState({
configuration_id: "",
configuration_label: ""
configuration_label: "",
});
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [filter, setFilter] = useState("");
@@ -82,12 +81,12 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
setSelectedConfiguration(element);
setEditScreenOpen(true);
}
}
}
},
},
];
const filteredRecords: IConfigurationElement[] = configurationElements.filter(
elementItem =>
(elementItem) =>
elementItem.configuration_id
.toLocaleLowerCase()
.includes(filter.toLocaleLowerCase())
@@ -99,7 +98,6 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
<EditConfiguration
open={editScreenOpen}
closeModalAndRefresh={() => {
setIsLoading(true);
setEditScreenOpen(false);
}}
selectedConfiguration={selectedConfiguration}
@@ -119,7 +117,7 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
className={classes.searchField}
id="search-resource"
label=""
onChange={event => {
onChange={(event) => {
setFilter(event.target.value);
}}
InputProps={{
@@ -128,7 +126,7 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
/>
</Grid>
@@ -139,9 +137,9 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Configuration", elementKey: "configuration_id" }
{ label: "Configuration", elementKey: "configuration_id" },
]}
isLoading={isLoading}
isLoading={false}
records={filteredRecords}
entityName="Configurations"
idField="configuration_id"

View File

@@ -44,29 +44,29 @@ interface IWebhook {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
boxShadow: "0px 3px 6px #00000012",
},
iconText: {
lineHeight: "24px"
}
lineHeight: "24px",
},
});
const panels = {
@@ -77,8 +77,8 @@ const panels = {
apiURL: "",
configuration: {
configuration_id: "logger_webhook",
configuration_label: "Logger Webhook"
}
configuration_label: "Logger Webhook",
},
},
audit: {
main: "audit",
@@ -87,9 +87,9 @@ const panels = {
apiURL: "",
configuration: {
configuration_id: "audit_webhook",
configuration_label: "Audit Webhook"
}
}
configuration_label: "Audit Webhook",
},
},
};
const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
@@ -107,15 +107,15 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
return null;
}
const filteredRecords: IWebhook[] = webhooks.filter(elementItem =>
const filteredRecords: IWebhook[] = webhooks.filter((elementItem) =>
elementItem.name.toLocaleLowerCase().includes(filter.toLocaleLowerCase())
);
const tableActions = [
{
type: "edit",
onClick: () => {}
}
onClick: () => {},
},
];
return (
@@ -144,7 +144,7 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
className={classes.searchField}
id="search-resource"
label=""
onChange={event => {
onChange={(event) => {
setFilter(event.target.value);
}}
InputProps={{
@@ -153,7 +153,7 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
/>
<Button

View File

@@ -21,13 +21,17 @@ import Grid from "@material-ui/core/Grid";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import { IElementValue } from "../types";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
interface IConfMySqlProps {
onChange: (newValue: IElementValue[]) => void;
classes: any;
}
const styles = (theme: Theme) => createStyles({});
const styles = (theme: Theme) =>
createStyles({
...modalBasic,
});
const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
//Local States
@@ -88,7 +92,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
{ key: "format", value: format },
{ key: "queue_dir", value: queueDir },
{ key: "queue_limit", value: queueLimit },
{ key: "comment", value: comment }
{ key: "comment", value: comment },
];
onChange(formValues);
@@ -101,7 +105,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
}, [user, dbName, password, port, host, setDsnString, configToDsnString]);
return (
<Grid container>
<Grid container className={classes.formScrollable}>
<Grid item xs={12}>
<FormControlLabel
control={
@@ -119,7 +123,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
"port",
"dbname",
"user",
"password"
"password",
]);
setHostname(kv.get("host") ? kv.get("host") + "" : "");
setPort(kv.get("port") ? kv.get("port") + "" : "");
@@ -137,6 +141,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
/>
}
label="Enter DSN String"
className={classes.formSlider}
/>
</Grid>
{useDsnString ? (
@@ -232,13 +237,13 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
id="format"
name="format"
label="Format"
onChange={e => {
onChange={(e) => {
setFormat(e.target.value);
}}
tooltip="'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'"
selectorOptions={[
{ label: "Namespace", value: "namespace" },
{ label: "Access", value: "access" }
{ label: "Access", value: "access" },
]}
/>
</Grid>
@@ -279,12 +284,6 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<br />
</Grid>
</Grid>
);
};

View File

@@ -22,13 +22,17 @@ import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBo
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { IElementValue } from "../types";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
interface IConfPostgresProps {
onChange: (newValue: IElementValue[]) => void;
classes: any;
}
const styles = (theme: Theme) => createStyles({});
const styles = (theme: Theme) =>
createStyles({
...modalBasic,
});
const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
//Local States
@@ -136,7 +140,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
{ key: "format", value: format },
{ key: "queue_dir", value: queueDir },
{ key: "queue_limit", value: queueLimit },
{ key: "comment", value: comment }
{ key: "comment", value: comment },
];
onChange(formValues);
@@ -148,7 +152,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
queueDir,
queueLimit,
comment,
onChange
onChange,
]);
useEffect(() => {
@@ -162,11 +166,11 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
sslMode,
host,
setConnectionString,
configToString
configToString,
]);
return (
<Grid container>
<Grid container className={classes.formScrollable}>
<Grid item xs={12}>
<FormControlLabel
control={
@@ -185,7 +189,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
"dbname",
"user",
"password",
"sslmode"
"sslmode",
]);
setHostname(kv.get("host") ? kv.get("host") + "" : "");
setPort(kv.get("port") ? kv.get("port") + "" : "");
@@ -206,6 +210,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
/>
}
label="Enter Connection String"
className={classes.formSlider}
/>
</Grid>
{useConnectionString ? (
@@ -273,7 +278,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
{ label: "Require", value: "require" },
{ label: "Disable", value: "disable" },
{ label: "Verify CA", value: "verify-ca" },
{ label: "Verify Full", value: "verify-full" }
{ label: "Verify Full", value: "verify-full" },
]}
/>
</Grid>
@@ -320,13 +325,13 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
id="format"
name="format"
label="Format"
onChange={e => {
onChange={(e) => {
setFormat(e.target.value);
}}
tooltip="'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'"
selectorOptions={[
{ label: "Namespace", value: "namespace" },
{ label: "Access", value: "access" }
{ label: "Access", value: "access" },
]}
/>
</Grid>
@@ -367,12 +372,6 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<br />
</Grid>
</Grid>
);
};

View File

@@ -15,35 +15,38 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useCallback, useEffect, useState } from "react";
import get from "lodash/get";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button } from "@material-ui/core";
import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import api from "../../../../common/api";
import { serverNeedsRestart } from "../../../../actions";
import { connect } from "react-redux";
import ConfTargetGeneric from "../ConfTargetGeneric";
import { serverNeedsRestart } from "../../../../actions";
import { fieldBasic } from "../../Common/FormComponents/common/styleLibrary";
import { fieldsConfigurations, removeEmptyFields } from "../utils";
import { IConfigurationElement, IElementValue } from "../types";
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
buttonContainer: {
textAlign: "right"
textAlign: "right",
},
logoButton: {
height: "80px"
}
height: "80px",
},
});
interface IAddNotificationEndpointProps {
@@ -59,19 +62,37 @@ const EditConfiguration = ({
closeModalAndRefresh,
serverNeedsRestart,
selectedConfiguration,
classes
classes,
}: IAddNotificationEndpointProps) => {
//Local States
const [valuesObj, setValueObj] = useState<IElementValue[]>([]);
const [saving, setSaving] = useState<boolean>(false);
const [addError, setError] = useState<string>("");
const [loadingConfig, setLoadingConfig] = useState<boolean>(true);
const [errorConfig, setErrorConfig] = useState<string>("");
const [configValues, setConfigValues] = useState<IElementValue[]>([]);
//Effects
useEffect(() => {
const configId = get(selectedConfiguration, "configuration_id", false);
if (configId) {
api
.invoke("GET", `/api/v1/configs/${configId}`)
.then((res) => {
const keyVals = get(res, "key_values", []);
setConfigValues(keyVals);
})
.catch((err) => {
setLoadingConfig(false);
setErrorConfig(err);
});
}
setLoadingConfig(false);
}, [selectedConfiguration]);
useEffect(() => {
if (saving) {
const payload = {
key_values: removeEmptyFields(valuesObj)
key_values: removeEmptyFields(valuesObj),
};
api
.invoke(
@@ -79,16 +100,16 @@ const EditConfiguration = ({
`/api/v1/configs/${selectedConfiguration.configuration_id}`,
payload
)
.then(res => {
.then((res) => {
setSaving(false);
setError("");
setErrorConfig("");
serverNeedsRestart(true);
closeModalAndRefresh();
})
.catch(err => {
.catch((err) => {
setSaving(false);
setError(err);
setErrorConfig(err);
});
}
}, [
@@ -96,7 +117,7 @@ const EditConfiguration = ({
serverNeedsRestart,
selectedConfiguration,
valuesObj,
closeModalAndRefresh
closeModalAndRefresh,
]);
//Fetch Actions
@@ -106,7 +127,7 @@ const EditConfiguration = ({
};
const onValueChange = useCallback(
newValue => {
(newValue) => {
setValueObj(newValue);
},
[setValueObj]
@@ -119,14 +140,14 @@ const EditConfiguration = ({
title={selectedConfiguration.configuration_label}
>
<React.Fragment>
{addError !== "" && (
{errorConfig !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
{errorConfig}
</Typography>
</Grid>
)}
@@ -136,18 +157,23 @@ const EditConfiguration = ({
fieldsConfigurations[selectedConfiguration.configuration_id]
}
onChange={onValueChange}
defaultVals={configValues}
/>
<Grid item xs={3} className={classes.buttonContainer}>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={saving}
>
Save
</Button>
</Grid>
{loadingConfig && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
<Grid item xs={9} />
</form>
</React.Fragment>

View File

@@ -31,36 +31,34 @@ export const configurationElements: IConfigurationElement[] = [
{ configuration_id: "cache", configuration_label: "Cache Configuration" },
{
configuration_id: "compression",
configuration_label: "Compression Configuration"
configuration_label: "Compression Configuration",
},
{ configuration_id: "etcd", configuration_label: "Etcd Configuration" },
{
configuration_id: "identity_openid",
configuration_label: "Identity Openid Configuration"
configuration_label: "Identity Openid Configuration",
},
{
configuration_id: "identity_ldap",
configuration_label: "Identity LDAP Configuration"
configuration_label: "Identity LDAP Configuration",
},
{
configuration_id: "policy_opa",
configuration_label: "Policy OPA Configuration"
configuration_label: "Policy OPA Configuration",
},
{
configuration_id: "kms_vault",
configuration_label: "KMS Vault Configuration"
configuration_label: "KMS Vault Configuration",
},
{ configuration_id: "kms_kes", configuration_label: "KMS KES Configuration" },
{
configuration_id: "logger_webhook",
configuration_label: "Logger Webhook Configuration",
url: "/webhook/logger"
},
{
configuration_id: "audit_webhook",
configuration_label: "Audit Webhook Configuration",
url: "/webhook/audit"
}
},
];
export const fieldsConfigurations: any = {
@@ -70,7 +68,7 @@ export const fieldsConfigurations: any = {
required: true,
label: "name",
tooltip: 'Name of the location of the server e.g. "us-west-rack2"',
type: "string"
type: "string",
},
{
name: "comment",
@@ -78,8 +76,8 @@ export const fieldsConfigurations: any = {
label: "comment",
tooltip: "You can add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
cache: [
{
@@ -88,21 +86,21 @@ export const fieldsConfigurations: any = {
label: "Drives",
tooltip:
'Mountpoints e.g. "/optane1" or "/optane2", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "expiry",
required: false,
label: "Expiry",
tooltip: 'Cache expiry duration in days e.g. "90"',
type: "number"
type: "number",
},
{
name: "quota",
required: false,
label: "Quota",
tooltip: 'Limit cache drive usage in percentage e.g. "90"',
type: "number"
type: "number",
},
{
name: "exclude",
@@ -110,14 +108,28 @@ export const fieldsConfigurations: any = {
label: "Exclude",
tooltip:
'Wildcard exclusion patterns e.g. "bucket/*.tmp" or "*.exe", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "after",
required: false,
label: "After",
tooltip: "Minimum number of access before caching an object",
type: "number"
type: "number",
},
{
name: "watermark_low",
required: false,
label: "Watermark Low",
tooltip: "Watermark Low",
type: "number",
},
{
name: "watermark_high",
required: false,
label: "Watermark High",
tooltip: "Watermark High",
type: "number",
},
{
name: "comment",
@@ -125,23 +137,17 @@ export const fieldsConfigurations: any = {
label: "Comment",
tooltip: "You can add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
compression: [
{
name: "minio_compress",
required: true,
label: "MinIO Compress",
type: "on|off"
},
{
name: "extensions",
required: false,
label: "Extensions",
tooltip:
'Extensions to compress e.g. ".txt",".log" or ".csv", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "mime_types",
@@ -149,8 +155,8 @@ export const fieldsConfigurations: any = {
label: "Mime Types",
tooltip:
'Mime types e.g. "text/*","application/json" or "application/xml", you can write one per field',
type: "csv"
}
type: "csv",
},
],
etcd: [
{
@@ -159,35 +165,35 @@ export const fieldsConfigurations: any = {
label: "Endpoints",
tooltip:
'List of etcd endpoints e.g. "http://localhost:2379", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "path_prefix",
required: false,
label: "Path Prefix",
tooltip: 'namespace prefix to isolate tenants e.g. "customer1/"',
type: "string"
type: "string",
},
{
name: "coredns_path",
required: false,
label: "Coredns Path",
tooltip: 'Shared bucket DNS records, default is "/skydns"',
type: "string"
type: "string",
},
{
name: "client_cert",
required: false,
label: "Client Cert",
tooltip: "Client cert for mTLS authentication",
type: "string"
type: "string",
},
{
name: "client_cert_key",
required: false,
label: "Client Cert Key",
tooltip: "Client cert key for mTLS authentication",
type: "string"
type: "string",
},
{
name: "comment",
@@ -195,23 +201,37 @@ export const fieldsConfigurations: any = {
label: "Comment",
tooltip: "You can add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
identity_openid: [
{
name: "client_id",
required: false,
label: "Client ID",
type: "string"
},
{
name: "config_url",
required: false,
label: "Config URL",
tooltip: "Config URL for Client ID configuration",
type: "string"
}
type: "string",
},
{
name: "client_id",
required: false,
label: "Client ID",
type: "string",
},
{
name: "claim_name",
required: false,
label: "Claim Name",
tooltip: "Claim Name",
type: "string",
},
{
name: "claim_prefix",
required: false,
label: "Claim Prefix",
tooltip: "Claim Prefix",
type: "string",
},
],
identity_ldap: [
{
@@ -219,7 +239,7 @@ export const fieldsConfigurations: any = {
required: true,
label: "Server ADDR",
tooltip: 'AD/LDAP server address e.g. "myldapserver.com:636"',
type: "string"
type: "string",
},
{
name: "username_format",
@@ -227,7 +247,7 @@ export const fieldsConfigurations: any = {
label: "Username Format",
tooltip:
'List of username bind DNs e.g. "uid=%s","cn=accounts","dc=myldapserver" or "dc=com", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "username_search_filter",
@@ -235,7 +255,7 @@ export const fieldsConfigurations: any = {
label: "Username Search Filter",
tooltip:
'User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"',
type: "string"
type: "string",
},
{
name: "group_search_filter",
@@ -243,21 +263,21 @@ export const fieldsConfigurations: any = {
label: "Group Search Filter",
tooltip:
'Search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))"',
type: "string"
type: "string",
},
{
name: "username_search_base_dn",
required: false,
label: "Username Search Base DN",
tooltip: "List of username search DNs, you can write one per field",
type: "csv"
type: "csv",
},
{
name: "group_name_attribute",
required: false,
label: "Group Name Attribute",
tooltip: 'Search attribute for group name e.g. "cn"',
type: "string"
type: "string",
},
{
name: "sts_expiry",
@@ -265,7 +285,7 @@ export const fieldsConfigurations: any = {
label: "STS Expiry",
tooltip:
'temporary credentials validity duration in s,m,h,d. Default is "1h"',
type: "string"
type: "string",
},
{
name: "tls_skip_verify",
@@ -273,7 +293,7 @@ export const fieldsConfigurations: any = {
label: "TLS Skip Verify",
tooltip:
'Trust server TLS without verification, defaults to "off" (verify)',
type: "on|off"
type: "on|off",
},
{
name: "server_insecure",
@@ -281,7 +301,7 @@ export const fieldsConfigurations: any = {
label: "Server Insecure",
tooltip:
'Allow plain text connection to AD/LDAP server, defaults to "off"',
type: "on|off"
type: "on|off",
},
{
name: "comment",
@@ -289,61 +309,59 @@ export const fieldsConfigurations: any = {
label: "Comment",
tooltip: "Optionally add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
policy_opa: [
{
name: "opa_url",
name: "url",
required: true,
label: "OPA URL",
type: "string"
}
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: [
{
name: "name",
name: "endpoint",
required: true,
label: "Name",
tooltip: "Name of the webhook",
type: "string"
label: "Endpoint",
type: "string",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string"
type: "string",
},
{
name: "endpoint",
required: true,
label: "Endpoint",
type: "string"
}
],
audit_webhook: [
{
name: "name",
name: "endpoint",
required: true,
label: "Name",
tooltip: "Name of the webhook",
type: "string"
label: "Endpoint",
type: "string",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string"
type: "string",
},
{
name: "endpoint",
required: true,
label: "Endpoint",
type: "string"
}
]
],
};
const commonFields = [
@@ -353,7 +371,7 @@ const commonFields = [
required: true,
tooltip: "staging dir for undelivered messages e.g. '/home/events'",
type: "string"
type: "string",
},
{
name: "queue-limit",
@@ -361,15 +379,15 @@ const commonFields = [
required: false,
tooltip: "maximum limit for undelivered messages, defaults to '10000'",
type: "number"
type: "number",
},
{
name: "comment",
label: "Comment",
required: false,
type: "string",
multiline: true
}
multiline: true,
},
];
export const notificationEndpointsFields: any = {
@@ -380,77 +398,77 @@ export const notificationEndpointsFields: any = {
required: true,
tooltip: "comma separated list of Kafka broker addresses",
type: "string"
type: "string",
},
{
name: "topic",
label: "Topic",
tooltip: "Kafka topic used for bucket notifications",
type: "string"
type: "string",
},
{
name: "sasl_username",
label: "SASL Username",
tooltip: "username for SASL/PLAIN or SASL/SCRAM authentication",
type: "string"
type: "string",
},
{
name: "sasl_password",
label: "SASL Password",
tooltip: "password for SASL/PLAIN or SASL/SCRAM authentication",
type: "string"
type: "string",
},
{
name: "sasl_mechanism",
label: "SASL Mechanism",
tooltip: "sasl authentication mechanism, default 'PLAIN'",
type: "string"
type: "string",
},
{
name: "tls_client_auth",
label: "TLS Client Auth",
tooltip:
"clientAuth determines the Kafka server's policy for TLS client auth",
type: "string"
type: "string",
},
{
name: "sasl",
label: "SASL",
tooltip: "set to 'on' to enable SASL authentication",
type: "on|off"
type: "on|off",
},
{
name: "tls",
label: "TLS",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
type: "on|off",
},
{
name: "tls_skip_verify",
label: "TLS skip verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
type: "on|off",
},
{
name: "client_tls_cert",
label: "client TLS cert",
tooltip: "path to client certificate for mTLS auth",
type: "path"
type: "path",
},
{
name: "client_tls_key",
label: "client TLS key",
tooltip: "path to client key for mTLS auth",
type: "path"
type: "path",
},
{
name: "version",
label: "Version",
tooltip: "specify the version of the Kafka cluster e.g '2.2.0'",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyAmqp]: [
{
@@ -459,68 +477,68 @@ export const notificationEndpointsFields: any = {
label: "url",
tooltip:
"AMQP server endpoint e.g. `amqp://myuser:mypassword@localhost:5672`",
type: "url"
type: "url",
},
{
name: "exchange",
label: "exchange",
tooltip: "name of the AMQP exchange",
type: "string"
type: "string",
},
{
name: "exchange_type",
label: "exchange_type",
tooltip: "AMQP exchange type",
type: "string"
type: "string",
},
{
name: "routing_key",
label: "routing_key",
tooltip: "routing key for publishing",
type: "string"
type: "string",
},
{
name: "mandatory",
label: "mandatory",
tooltip:
"quietly ignore undelivered messages when set to 'off', default is 'on'",
type: "on|off"
type: "on|off",
},
{
name: "durable",
label: "durable",
tooltip:
"persist queue across broker restarts when set to 'on', default is 'off'",
type: "on|off"
type: "on|off",
},
{
name: "no_wait",
label: "no_wait",
tooltip:
"non-blocking message delivery when set to 'on', default is 'off'",
type: "on|off"
type: "on|off",
},
{
name: "internal",
label: "internal",
tooltip:
"set to 'on' for exchange to be not used directly by publishers, but only when bound to other exchanges",
type: "on|off"
type: "on|off",
},
{
name: "auto_deleted",
label: "auto_deleted",
tooltip:
"auto delete queue when set to 'on', when there are no consumers",
type: "on|off"
type: "on|off",
},
{
name: "delivery_mode",
label: "delivery_mode",
tooltip: "set to '1' for non-persistent or '2' for persistent queue",
type: "number"
type: "number",
},
...commonFields
...commonFields,
],
[notifyRedis]: [
{
@@ -528,22 +546,22 @@ export const notificationEndpointsFields: any = {
required: true,
label: "address",
tooltip: "Redis server's address. For example: `localhost:6379`",
type: "address"
type: "address",
},
{
name: "key",
required: true,
label: "key",
tooltip: "Redis key to store/update events, key is auto-created",
type: "string"
type: "string",
},
{
name: "password",
label: "password",
tooltip: "Redis server password",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyMqtt]: [
{
@@ -551,46 +569,46 @@ export const notificationEndpointsFields: any = {
required: true,
label: "broker",
tooltip: "MQTT server endpoint e.g. `tcp://localhost:1883`",
type: "uri"
type: "uri",
},
{
name: "topic",
required: true,
label: "topic",
tooltip: "name of the MQTT topic to publish",
type: "string"
type: "string",
},
{
name: "username",
label: "username",
tooltip: "MQTT username",
type: "string"
type: "string",
},
{
name: "password",
label: "password",
tooltip: "MQTT password",
type: "string"
type: "string",
},
{
name: "qos",
label: "qos",
tooltip: "set the quality of service priority, defaults to '0'",
type: "number"
type: "number",
},
{
name: "keep_alive_interval",
label: "keep_alive_interval",
tooltip: "keep-alive interval for MQTT connections in s,m,h,d",
type: "duration"
type: "duration",
},
{
name: "reconnect_interval",
label: "reconnect_interval",
tooltip: "reconnect interval for MQTT connections in s,m,h,d",
type: "duration"
type: "duration",
},
...commonFields
...commonFields,
],
[notifyNats]: [
{
@@ -598,95 +616,95 @@ export const notificationEndpointsFields: any = {
required: true,
label: "address",
tooltip: "NATS server address e.g. '0.0.0.0:4222'",
type: "address"
type: "address",
},
{
name: "subject",
required: true,
label: "subject",
tooltip: "NATS subscription subject",
type: "string"
type: "string",
},
{
name: "username",
label: "username",
tooltip: "NATS username",
type: "string"
type: "string",
},
{
name: "password",
label: "password",
tooltip: "NATS password",
type: "string"
type: "string",
},
{
name: "token",
label: "token",
tooltip: "NATS token",
type: "string"
type: "string",
},
{
name: "tls",
label: "tls",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
type: "on|off",
},
{
name: "tls_skip_verify",
label: "tls_skip_verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
type: "on|off",
},
{
name: "ping_interval",
label: "ping_interval",
tooltip: "client ping commands interval in s,m,h,d. Disabled by default",
type: "duration"
type: "duration",
},
{
name: "streaming",
label: "streaming",
tooltip: "set to 'on', to use streaming NATS server",
type: "on|off"
type: "on|off",
},
{
name: "streaming_async",
label: "streaming_async",
tooltip: "set to 'on', to enable asynchronous publish",
type: "on|off"
type: "on|off",
},
{
name: "streaming_max_pub_acks_in_flight",
label: "streaming_max_pub_acks_in_flight",
tooltip: "number of messages to publish without waiting for ACKs",
type: "number"
type: "number",
},
{
name: "streaming_cluster_id",
label: "streaming_cluster_id",
tooltip: "unique ID for NATS streaming cluster",
type: "string"
type: "string",
},
{
name: "cert_authority",
label: "cert_authority",
tooltip: "path to certificate chain of the target NATS server",
type: "string"
type: "string",
},
{
name: "client_cert",
label: "client_cert",
tooltip: "client cert for NATS mTLS auth",
type: "string"
type: "string",
},
{
name: "client_key",
label: "client_key",
tooltip: "client cert key for NATS mTLS auth",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyElasticsearch]: [
{
@@ -695,7 +713,7 @@ export const notificationEndpointsFields: any = {
label: "url",
tooltip:
"Elasticsearch server's address, with optional authentication info",
type: "url"
type: "url",
},
{
name: "index",
@@ -703,7 +721,7 @@ export const notificationEndpointsFields: any = {
label: "index",
tooltip:
"Elasticsearch index to store/update events, index is auto-created",
type: "string"
type: "string",
},
{
name: "format",
@@ -711,9 +729,9 @@ export const notificationEndpointsFields: any = {
label: "format",
tooltip:
"'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'",
type: "enum"
type: "enum",
},
...commonFields
...commonFields,
],
[notifyWebhooks]: [
{
@@ -722,15 +740,15 @@ export const notificationEndpointsFields: any = {
label: "endpoint",
tooltip:
"webhook server endpoint e.g. http://localhost:8080/minio/events",
type: "url"
type: "url",
},
{
name: "auth_token",
label: "auth_token",
tooltip: "opaque string or JWT authorization token",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyNsq]: [
{
@@ -738,34 +756,34 @@ export const notificationEndpointsFields: any = {
required: true,
label: "nsqd_address",
tooltip: "NSQ server address e.g. '127.0.0.1:4150'",
type: "address"
type: "address",
},
{
name: "topic",
required: true,
label: "topic",
tooltip: "NSQ topic",
type: "string"
type: "string",
},
{
name: "tls",
label: "tls",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
type: "on|off",
},
{
name: "tls_skip_verify",
label: "tls_skip_verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
type: "on|off",
},
...commonFields
]
...commonFields,
],
};
export const removeEmptyFields = (formFields: IElementValue[]) => {
const nonEmptyFields = formFields.filter(field => field.value !== "");
const nonEmptyFields = formFields.filter((field) => field.value !== "");
return nonEmptyFields;
};

View File

@@ -14,13 +14,13 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import React, { useEffect } from "react";
import clsx from "clsx";
import {
createStyles,
StyledProps,
Theme,
withStyles
withStyles,
} from "@material-ui/core/styles";
import CssBaseline from "@material-ui/core/CssBaseline";
import Drawer from "@material-ui/core/Drawer";
@@ -28,7 +28,6 @@ import Box from "@material-ui/core/Box";
import Typography from "@material-ui/core/Typography";
import Container from "@material-ui/core/Container";
import Link from "@material-ui/core/Link";
import history from "../../history";
import {
Redirect,
@@ -36,21 +35,21 @@ import {
RouteComponentProps,
Router,
Switch,
withRouter
withRouter,
} from "react-router-dom";
import { connect } from "react-redux";
import { AppState } from "../../store";
import {
serverIsLoading,
serverNeedsRestart,
setMenuOpen
setMenuOpen,
} from "../../actions";
import { ThemedComponentProps } from "@material-ui/core/styles/withTheme";
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,6 +61,13 @@ import ConfigurationsList from "./Configurations/ConfigurationPanels/Configurati
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 (
@@ -81,43 +87,43 @@ const drawerWidth = 254;
const styles = (theme: Theme) =>
createStyles({
root: {
display: "flex"
display: "flex",
},
toolbar: {
background: theme.palette.background.default,
color: "black",
paddingRight: 24 // keep right padding when drawer closed
paddingRight: 24, // keep right padding when drawer closed
},
toolbarIcon: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: "0 8px",
...theme.mixins.toolbar
...theme.mixins.toolbar,
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
})
duration: theme.transitions.duration.leavingScreen,
}),
},
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen
})
duration: theme.transitions.duration.enteringScreen,
}),
},
menuButton: {
marginRight: 36
marginRight: 36,
},
menuButtonHidden: {
display: "none"
display: "none",
},
title: {
flexGrow: 1
flexGrow: 1,
},
drawerPaper: {
position: "relative",
@@ -125,40 +131,41 @@ const styles = (theme: Theme) =>
width: drawerWidth,
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen
})
duration: theme.transitions.duration.enteringScreen,
}),
overflowX: "hidden",
},
drawerPaperClose: {
overflowX: "hidden",
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
duration: theme.transitions.duration.leavingScreen,
}),
width: theme.spacing(7),
[theme.breakpoints.up("sm")]: {
width: theme.spacing(9)
}
width: theme.spacing(9),
},
},
appBarSpacer: {
height: "5px"
height: "5px",
},
content: {
flexGrow: 1,
height: "100vh",
overflow: "auto"
overflow: "auto",
},
container: {
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4)
paddingBottom: theme.spacing(4),
},
paper: {
padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
fixedHeight: {
minHeight: 240
minHeight: 240,
},
warningBar: {
background: theme.palette.primary.main,
@@ -166,22 +173,10 @@ const styles = (theme: Theme) =>
heigh: "60px",
widht: "100%",
lineHeight: "60px",
textAlign: "center"
}
textAlign: "center",
},
});
const mapState = (state: AppState) => ({
open: state.system.sidebarOpen,
needsRestart: state.system.serverNeedsRestart,
isServerLoading: state.system.serverIsLoading
});
const connector = connect(mapState, {
setMenuOpen,
serverNeedsRestart,
serverIsLoading
});
interface IConsoleProps {
open: boolean;
needsRestart: boolean;
@@ -191,141 +186,218 @@ interface IConsoleProps {
setMenuOpen: typeof setMenuOpen;
serverNeedsRestart: typeof serverNeedsRestart;
serverIsLoading: typeof serverIsLoading;
saveSessionResponse: typeof saveSessionResponse;
session: ISessionResponse;
}
class Console extends React.Component<
IConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps
> {
componentDidMount(): void {
const Console = ({
classes,
open,
needsRestart,
isServerLoading,
serverNeedsRestart,
serverIsLoading,
saveSessionResponse,
session,
}: IConsoleProps) => {
useEffect(() => {
api
.invoke("GET", `/api/v1/session`)
.then(res => {
console.log(res);
.then((res) => {
saveSessionResponse(res);
})
.catch(err => {
.catch((err) => {
storage.removeItem("token");
history.push("/");
});
}
}, [saveSessionResponse]);
restartServer() {
this.props.serverIsLoading(true);
const restartServer = () => {
serverIsLoading(true);
api
.invoke("POST", "/api/v1/service/restart", {})
.then(res => {
.then((res) => {
console.log("success restarting service");
console.log(res);
this.props.serverIsLoading(false);
this.props.serverNeedsRestart(false);
serverIsLoading(false);
serverNeedsRestart(false);
})
.catch(err => {
this.props.serverIsLoading(false);
.catch((err) => {
serverIsLoading(false);
console.log("failure restarting service");
console.log(err);
});
}
};
render() {
const { classes, open, needsRestart, isServerLoading } = this.props;
return (
<div className={classes.root}>
<CssBaseline />
<Drawer
variant="permanent"
classes={{
paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose)
}}
open={open}
>
{/*<div className={classes.toolbarIcon}>*/}
{/* <IconButton*/}
{/* onClick={() => {*/}
{/* this.props.setMenuOpen(false);*/}
{/* }}*/}
{/* >*/}
{/* <ChevronLeftIcon />*/}
{/* </IconButton>*/}
{/*</div>*/}
{/*<Divider />*/}
const allowedPages = session.pages.reduce(
(result: any, item: any, index: any) => {
result[item] = true;
return result;
},
{}
);
const routes = [
{
component: Dashboard,
path: "/dashboard",
},
{
component: Buckets,
path: "/buckets",
},
{
component: Buckets,
path: "/buckets/:bucketName",
},
{
component: Watch,
path: "/watch",
},
{
component: Users,
path: "/users",
},
{
component: Groups,
path: "/groups",
},
{
component: Policies,
path: "/policies",
},
{
component: Trace,
path: "/trace",
},
{
component: Logs,
path: "/logs",
},
{
component: Heal,
path: "/heal",
},
{
component: ListNotificationEndpoints,
path: "/notification-endpoints",
},
{
component: ConfigurationsList,
path: "/configurations-list",
},
{
component: Permissions,
path: "/permissions",
},
{
component: ServiceAccounts,
path: "/service-accounts",
},
{
component: WebhookPanel,
path: "/webhook/logger",
},
{
component: WebhookPanel,
path: "/webhook/audit",
},
{
component: ListTenants,
path: "/tenants",
},
{
component: TenantDetails,
path: "/clusters/:clusterName",
},
];
const allowedRoutes = routes.filter((route: any) => allowedPages[route.path]);
<Menu />
</Drawer>
return (
<React.Fragment>
{session.status == "ok" ? (
<div className={classes.root}>
<CssBaseline />
<Drawer
variant="permanent"
classes={{
paper: clsx(
classes.drawerPaper,
!open && classes.drawerPaperClose
),
}}
open={open}
>
<Menu pages={session.pages} />
</Drawer>
<main className={classes.content}>
{needsRestart && (
<div className={classes.warningBar}>
{isServerLoading ? (
<React.Fragment>
The server is restarting.
<LinearProgress />
</React.Fragment>
) : (
<React.Fragment>
The instance needs to be restarted for configuration changes
to take effect.{" "}
<Button
color="secondary"
size="small"
onClick={() => {
this.restartServer();
}}
>
Restart
</Button>
</React.Fragment>
)}
</div>
)}
<div className={classes.appBarSpacer} />
<Container maxWidth="lg" className={classes.container}>
<Router history={history}>
<Switch>
<Route path="/buckets" component={Buckets} />
<Route exact path="/permissions" component={Permissions} />
<Route exact path="/policies" component={Policies} />
<Route
exact
path="/service_accounts"
component={ServiceAccounts}
/>
<Route exact path="/users" component={Users} />
<Route exact path="/dashboard" component={Dashboard} />
<Route exct path="/groups" component={Groups} />
<Route
exact
path="/notification-endpoints"
component={ListNotificationEndpoints}
/>
<Route
exact
path="/configurations-list"
component={ConfigurationsList}
/>
<Route exact path="/webhook/logger" component={WebhookPanel} />
<Route exact path="/webhook/audit" component={WebhookPanel} />
<Route exct path="/trace" component={Trace} />
<Route exact path="/">
<Redirect to="/dashboard" />
</Route>
<Route component={NotFoundPage} />
</Switch>
</Router>
<main className={classes.content}>
{needsRestart && (
<div className={classes.warningBar}>
{isServerLoading ? (
<React.Fragment>
The server is restarting.
<LinearProgress />
</React.Fragment>
) : (
<React.Fragment>
The instance needs to be restarted for configuration changes
to take effect.{" "}
<Button
color="secondary"
size="small"
onClick={() => {
restartServer();
}}
>
Restart
</Button>
</React.Fragment>
)}
</div>
)}
<div className={classes.appBarSpacer} />
<Container maxWidth="lg" className={classes.container}>
<Router history={history}>
<Switch>
{allowedRoutes.map((route: any) => (
<Route
key={route.path}
exact
path={route.path}
component={route.component}
/>
))}
{allowedRoutes.length > 0 ? (
<Route exact path="/">
<Redirect to={allowedRoutes[0].path} />
</Route>
) : null}
</Switch>
</Router>
<Box pt={4}>
<Copyright />
</Box>
</Container>
</main>
</div>
);
}
}
<Box pt={4}>
<Copyright />
</Box>
</Container>
</main>
</div>
) : null}
</React.Fragment>
);
};
// );
const mapState = (state: AppState) => ({
open: state.system.sidebarOpen,
needsRestart: state.system.serverNeedsRestart,
isServerLoading: state.system.serverIsLoading,
session: state.console.session,
});
export default withRouter(connector(withStyles(styles)(Console)));
// export default withStyles(styles)(connector(Console));
// export default compose(
// withStyles(styles),
// connector
// )(withRouter(Console))
const connector = connect(mapState, {
setMenuOpen,
serverNeedsRestart,
serverIsLoading,
saveSessionResponse,
});
export default connector(withStyles(styles)(Console));

View File

@@ -26,86 +26,87 @@ import ViewHeadlineIcon from "@material-ui/icons/ViewHeadline";
import { Usage } from "./types";
import api from "../../../common/api";
import { niceBytes } from "../../../common/utils";
import { LinearProgress } from "@material-ui/core";
const styles = (theme: Theme) =>
createStyles({
root: {
display: "flex"
display: "flex",
},
toolbar: {
paddingRight: 24 // keep right padding when drawer closed
paddingRight: 24, // keep right padding when drawer closed
},
toolbarIcon: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: "0 8px",
...theme.mixins.toolbar
...theme.mixins.toolbar,
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
})
duration: theme.transitions.duration.leavingScreen,
}),
},
menuButton: {
marginRight: 36
marginRight: 36,
},
menuButtonHidden: {
display: "none"
display: "none",
},
title: {
flexGrow: 1
flexGrow: 1,
},
drawerPaperClose: {
overflowX: "hidden",
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
duration: theme.transitions.duration.leavingScreen,
}),
width: theme.spacing(7),
[theme.breakpoints.up("sm")]: {
width: theme.spacing(9)
}
width: theme.spacing(9),
},
},
appBarSpacer: theme.mixins.toolbar,
content: {
flexGrow: 1,
height: "100vh",
overflow: "auto"
overflow: "auto",
},
container: {
paddingBottom: theme.spacing(4),
"& h6": {
color: "#777777",
fontSize: 14
fontSize: 14,
},
"& p": {
"& span": {
fontSize: 16
}
}
fontSize: 16,
},
},
},
paper: {
padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
fixedHeight: {
minHeight: 240
minHeight: 240,
},
consumptionValue: {
color: "#000000",
fontSize: "60px",
fontWeight: "bold"
fontWeight: "bold",
},
icon: {
marginRight: 10,
color: "#777777"
}
color: "#777777",
},
});
interface IDashboardProps {
@@ -115,7 +116,7 @@ interface IDashboardProps {
const Dashboard = ({ classes }: IDashboardProps) => {
const fixedHeightPaper = clsx(classes.paper, classes.fixedHeight);
const [usage, setUsage] = useState<Usage | null>(null);
const [loading, isLoading] = useState<boolean>(true);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
useEffect(() => {
@@ -130,11 +131,11 @@ const Dashboard = ({ classes }: IDashboardProps) => {
.then((res: Usage) => {
setUsage(res);
setError("");
isLoading(false);
setLoading(false);
})
.catch(err => {
.catch((err) => {
setError(err);
isLoading(false);
setLoading(false);
});
};
const prettyUsage = (usage: string | undefined) => {
@@ -143,7 +144,6 @@ const Dashboard = ({ classes }: IDashboardProps) => {
}
return niceBytes(usage);
};
const units = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const prettyNumber = (usage: number | undefined) => {
if (usage === undefined) {
@@ -161,51 +161,59 @@ const Dashboard = ({ classes }: IDashboardProps) => {
<Typography variant="h2">MinIO Console</Typography>
</Grid>
{error !== "" && <Grid container>{error}</Grid>}
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<ViewHeadlineIcon />
</Grid>
<Grid item>
<Typography variant="h6">Total Buckets</Typography>
</Grid>
{loading ? (
<Grid item xs={12} md={12} lg={12}>
<LinearProgress />
</Grid>
) : (
<React.Fragment>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<ViewHeadlineIcon />
</Grid>
<Grid item>
<Typography variant="h6">Total Buckets</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyNumber(usage.buckets) : 0}
</Typography>
</Paper>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyNumber(usage.buckets) : 0}
</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<NetworkCheckIcon />
</Grid>
<Grid item>
<Typography variant="h6"> Total Objects</Typography>
</Grid>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<NetworkCheckIcon />
</Grid>
<Grid item>
<Typography variant="h6"> Total Objects</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyNumber(usage.objects) : 0}
</Typography>
</Paper>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyNumber(usage.objects) : 0}
</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<PieChartIcon />
</Grid>
<Grid item>
<Typography variant="h6">Usage</Typography>
</Grid>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<PieChartIcon />
</Grid>
<Grid item>
<Typography variant="h6">Usage</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyUsage(usage.usage + "") : 0}
</Typography>
</Paper>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyUsage(usage.usage + "") : 0}
</Typography>
</Paper>
</Grid>
</React.Fragment>
)}
</Grid>
</Grid>
</React.Fragment>

View File

@@ -19,6 +19,7 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import api from "../../../common/api";
import UsersSelectors from "./UsersSelectors";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
@@ -41,24 +42,25 @@ interface MainGroupProps {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
buttonContainer: {
textAlign: "right"
}
textAlign: "right",
},
...modalBasic,
});
const AddGroup = ({
open,
selectedGroup,
closeModalAndRefresh,
classes
classes,
}: IGroupProps) => {
//Local States
const [groupName, setGroupName] = useState<string>("");
@@ -86,14 +88,14 @@ const AddGroup = ({
.invoke("PUT", `/api/v1/groups/${groupName}`, {
group: groupName,
members: selectedUsers,
status: groupEnabled
status: groupEnabled,
})
.then(res => {
.then((res) => {
isSaving(false);
setError("");
closeModalAndRefresh();
})
.catch(err => {
.catch((err) => {
isSaving(false);
setError(err);
});
@@ -101,14 +103,14 @@ const AddGroup = ({
api
.invoke("POST", "/api/v1/groups", {
group: groupName,
members: selectedUsers
members: selectedUsers,
})
.then(res => {
.then((res) => {
isSaving(false);
setError("");
closeModalAndRefresh();
})
.catch(err => {
.catch((err) => {
isSaving(false);
setError(err);
});
@@ -122,7 +124,7 @@ const AddGroup = ({
selectedUsers,
groupEnabled,
selectedGroup,
closeModalAndRefresh
closeModalAndRefresh,
]);
useEffect(() => {
@@ -135,7 +137,7 @@ const AddGroup = ({
setGroupName(res.name);
setSelectedUsers(res.members);
})
.catch(err => {
.catch((err) => {
setError(err);
isLoadingGroup(false);
});
@@ -159,62 +161,61 @@ const AddGroup = ({
>
<form noValidate autoComplete="off" onSubmit={setSaving}>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
{selectedGroup !== null ? (
<React.Fragment>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={groupEnabled}
id="group-status"
name="group-status"
label="Status"
onChange={e => {
setGroupEnabled(e.target.value);
}}
selectorOptions={[
{ label: "Enabled", value: "enabled" },
{ label: "Disabled", value: "disabled" }
]}
/>
</Grid>
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="group-name"
name="group-name"
label="Name"
value={groupName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setGroupName(e.target.value);
}}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<UsersSelectors
selectedUsers={selectedUsers}
setSelectedUsers={setSelectedUsers}
/>
</Grid>
<Grid item xs={12}>
<br />
{selectedGroup !== null ? (
<React.Fragment>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={groupEnabled}
id="group-status"
name="group-status"
label="Status"
onChange={(e) => {
setGroupEnabled(e.target.value);
}}
selectorOptions={[
{ label: "Enabled", value: "enabled" },
{ label: "Disabled", value: "disabled" },
]}
/>
</Grid>
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="group-name"
name="group-name"
label="Name"
value={groupName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setGroupName(e.target.value);
}}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<UsersSelectors
selectedUsers={selectedUsers}
setSelectedUsers={setSelectedUsers}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button

View File

@@ -26,10 +26,11 @@ import { CreateIcon } from "../../../icons";
import api from "../../../common/api";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import { GroupsList } from "./types";
import { groupsSort } from "../../../utils/sortFunctions";
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;
@@ -39,50 +40,50 @@ interface IGroupsProps {
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
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),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
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",
},
});
const Groups = ({ classes }: IGroupsProps) => {
@@ -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);
@@ -126,7 +128,7 @@ const Groups = ({ classes }: IGroupsProps) => {
.then((res: GroupsList) => {
let resGroups: string[] = [];
if (res.groups !== null) {
resGroups = res.groups.sort(groupsSort);
resGroups = res.groups.sort(stringSort);
}
setRecords(resGroups);
const total = !res.total ? 0 : res.total;
@@ -140,7 +142,7 @@ const Groups = ({ classes }: IGroupsProps) => {
setPage(newPage);
}
})
.catch(err => {
.catch((err) => {
setError(err);
isLoading(false);
});
@@ -162,7 +164,7 @@ const Groups = ({ classes }: IGroupsProps) => {
}
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.includes(filter)
);
@@ -176,9 +178,15 @@ const Groups = ({ classes }: IGroupsProps) => {
setSelectedGroup(group);
};
const setPolicyAction = (selectionElement: any): void => {
setPolicyOpen(true);
setSelectedGroup(selectionElement);
};
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "delete", onClick: deleteAction }
{ type: "description", onClick: setPolicyAction },
{ type: "delete", onClick: deleteAction },
];
return (
@@ -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>
@@ -217,9 +235,9 @@ const Groups = ({ classes }: IGroupsProps) => {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
onChange={(e) => {
setFilter(e.target.value);
}}
/>
@@ -255,11 +273,11 @@ const Groups = ({ classes }: IGroupsProps) => {
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>

View File

@@ -38,43 +38,43 @@ interface IGroupsProps {
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
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),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "left",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
filterField: {
background: "#FFFFFF",
@@ -82,24 +82,24 @@ const styles = (theme: Theme) =>
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: "100%",
zIndex: 500
zIndex: 500,
},
noFound: {
textAlign: "center",
padding: "10px 0"
padding: "10px 0",
},
tableContainer: {
maxHeight: 250
maxHeight: 250,
},
stickyHeader: {
backgroundColor: "#fff"
}
backgroundColor: "#fff",
},
});
const UsersSelectors = ({
classes,
selectedUsers,
setSelectedUsers
setSelectedUsers,
}: IGroupsProps) => {
//Local States
const [records, setRecords] = useState<any[]>([]);
@@ -133,7 +133,7 @@ const UsersSelectors = ({
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
elements = elements.filter((element) => element !== value);
}
setSelectedUsers(elements);
@@ -154,13 +154,13 @@ const UsersSelectors = ({
setError("");
isLoading(false);
})
.catch(err => {
.catch((err) => {
setError(err);
isLoading(false);
});
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.accessKey.includes(filter)
);
@@ -185,9 +185,9 @@ const UsersSelectors = ({
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
onChange={(e) => {
setFilter(e.target.value);
}}
/>

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

@@ -0,0 +1,268 @@
// 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 } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { AppState } from "../../../store";
import { connect } from "react-redux";
import { logMessageReceived, logResetMessages } from "./actions";
import { LogMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { timeFromDate } from "../../../common/utils";
import { isNullOrUndefined } from "util";
import { wsProtocol } from "../../../utils/wsUtils";
const styles = (theme: Theme) =>
createStyles({
logList: {
background: "white",
maxHeight: "400px",
overflow: "auto",
"& ul": {
margin: "4px",
padding: "0px",
},
"& ul li": {
listStyle: "none",
margin: "0px",
padding: "0px",
borderBottom: "1px solid #dedede",
},
},
tab: {
padding: "25px",
},
logerror: {
color: "#A52A2A",
},
logerror_tab: {
color: "#A52A2A",
padding: "25px",
},
ansidefault: {
color: "black",
},
});
interface ILogs {
classes: any;
logMessageReceived: typeof logMessageReceived;
logResetMessages: typeof logResetMessages;
messages: LogMessage[];
}
const Logs = ({
classes,
logMessageReceived,
logResetMessages,
messages,
}: ILogs) => {
useEffect(() => {
logResetMessages();
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/console`
);
let interval: any | null = null;
if (c !== null) {
c.onopen = () => {
console.log("WebSocket Client Connected");
c.send("ok");
interval = setInterval(() => {
c.send("ok");
}, 10 * 1000);
};
c.onmessage = (message: IMessageEvent) => {
// console.log(message.data.toString())
let m: LogMessage = JSON.parse(message.data.toString());
m.time = new Date(m.time.toString());
m.key = Math.random();
logMessageReceived(m);
};
c.onclose = () => {
clearInterval(interval);
console.log("connection closed by server");
};
return () => {
c.close(1000);
clearInterval(interval);
console.log("closing websockets");
};
}
}, [logMessageReceived]);
// replaces a character of a string with other at a given index
const replaceWeirdChar = (
origString: string,
replaceChar: string,
index: number
) => {
let firstPart = origString.substr(0, index);
let lastPart = origString.substr(index + 1);
let newString = firstPart + replaceChar + lastPart;
return newString;
};
const renderError = (logElement: LogMessage) => {
let errorElems = [];
if (!isNullOrUndefined(logElement.error)) {
if (logElement.api && logElement.api.name) {
errorElems.push(
<li key={`api-${logElement.key}`}>
<span className={classes.logerror}>API: {logElement.api.name}</span>
</li>
);
}
if (logElement.time) {
errorElems.push(
<li key={`time-${logElement.key}`}>
<span className={classes.logerror}>
Time: {timeFromDate(logElement.time)}
</span>
</li>
);
}
if (logElement.deploymentid) {
errorElems.push(
<li key={`deploytmentid-${logElement.key}`}>
<span className={classes.logerror}>
DeploymentID: {logElement.deploymentid}
</span>
</li>
);
}
if (logElement.requestID) {
errorElems.push(
<li key={`requestid-${logElement.key}`}>
<span className={classes.logerror}>
RequestID: {logElement.requestID}
</span>
</li>
);
}
if (logElement.remotehost) {
errorElems.push(
<li key={`remotehost-${logElement.key}`}>
<span className={classes.logerror}>
RemoteHost: {logElement.remotehost}
</span>
</li>
);
}
if (logElement.host) {
errorElems.push(
<li key={`host-${logElement.key}`}>
<span className={classes.logerror}>Host: {logElement.host}</span>
</li>
);
}
if (logElement.userAgent) {
errorElems.push(
<li key={`useragent-${logElement.key}`}>
<span className={classes.logerror}>
UserAgent: {logElement.userAgent}
</span>
</li>
);
}
if (logElement.error.message) {
errorElems.push(
<li key={`message-${logElement.key}`}>
<span className={classes.logerror}>
Error: {logElement.error.message}
</span>
</li>
);
}
if (logElement.error.source) {
// for all sources add padding
for (let s in logElement.error.source) {
errorElems.push(
<li key={`source-${logElement.key}-${s}`}>
<span className={classes.logerror_tab}>
{logElement.error.source[s]}
</span>
</li>
);
}
}
}
return errorElems;
};
const renderLog = (logElement: LogMessage) => {
let logMessage = logElement.ConsoleMsg;
// remove any non ascii characters, exclude any control codes
logMessage = logMessage.replace(/([^\x20-\x7F])/g, "");
// regex for terminal colors like e.g. `[31;4m `
const tColorRegex = /((\[[0-9;]+m))/g;
// get substring if there was a match for to split what
// is going to be colored and what not, here we add color
// only to the first match.
let substr = logMessage.replace(tColorRegex, "");
// if starts with multiple spaces add padding
if (substr.startsWith(" ")) {
return (
<li key={logElement.key}>
<span className={classes.tab}>{substr}</span>
</li>
);
} else if (!isNullOrUndefined(logElement.error)) {
// list error message and all sources and error elems
return renderError(logElement);
} else {
// for all remaining set default class
return (
<li key={logElement.key}>
<span className={classes.ansidefault}>{substr}</span>
</li>
);
}
};
return (
<div>
<h1>Logs</h1>
<div className={classes.logList}>
<ul>
{messages.map((m) => {
return renderLog(m);
})}
</ul>
</div>
</div>
);
};
const mapState = (state: AppState) => ({
messages: state.logs.messages,
});
const connector = connect(mapState, {
logMessageReceived: logMessageReceived,
logResetMessages: logResetMessages,
});
export default connector(withStyles(styles)(Logs));

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/>.
import { LogMessage } from "./types";
export const LOG_MESSAGE_RECEIVED = "LOG_MESSAGE_RECEIVED";
export const LOG_RESET_MESSAGES = "LOG_RESET_MESSAGES";
interface LogMessageReceivedAction {
type: typeof LOG_MESSAGE_RECEIVED;
message: LogMessage;
}
interface LogResetMessagesAction {
type: typeof LOG_RESET_MESSAGES;
}
export type LogActionTypes = LogMessageReceivedAction | LogResetMessagesAction;
export function logMessageReceived(message: LogMessage) {
return {
type: LOG_MESSAGE_RECEIVED,
message: message,
};
}
export function logResetMessages() {
return {
type: LOG_RESET_MESSAGES,
};
}

View File

@@ -0,0 +1,50 @@
// 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 {
LOG_MESSAGE_RECEIVED,
LOG_RESET_MESSAGES,
LogActionTypes,
} from "./actions";
import { LogMessage } from "./types";
export interface LogState {
messages: LogMessage[];
}
const initialState: LogState = {
messages: [],
};
export function logReducer(
state = initialState,
action: LogActionTypes
): LogState {
switch (action.type) {
case LOG_MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message],
};
case LOG_RESET_MESSAGES:
return {
...state,
messages: [],
};
default:
return state;
}
}

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 logError {
message: string;
source: string[];
}
export interface logErrorApiArgs {
bucket: string;
object: string;
}
export interface logErrorApi {
name: string;
args: logErrorApiArgs;
}
export interface LogMessage {
remotehost: string;
host: string;
requestID: string;
userAgent: string;
message: string;
api: logErrorApi;
deploymentid: string;
time: Date;
error: logError;
ConsoleMsg: string;
key: number;
}

View File

@@ -1,191 +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 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;
}
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 } = this.props;
return (
<React.Fragment>
<div className={classes.logo}>
<img src={logo} alt="logo" />
</div>
<List className={classes.menuList}>
<ListItem button component={NavLink} to="/dashboard">
<ListItemIcon>
<DashboardIcon />
</ListItemIcon>
<ListItemText primary="Dashboard" />
</ListItem>
<ListItem button component={NavLink} to="/buckets">
<ListItemIcon>
<BucketsIcon />
</ListItemIcon>
<ListItemText primary="Buckets" />
</ListItem>
<Divider />
<ListItem component={Typography}>Admin</ListItem>
<ListItem button component={NavLink} to="/users">
<ListItemIcon>
<PersonIcon />
</ListItemIcon>
<ListItemText primary="Users" />
</ListItem>
<ListItem button component={NavLink} to="/groups">
<ListItemIcon>
<UsersIcon />
</ListItemIcon>
<ListItemText primary="Groups" />
</ListItem>
<ListItem button component={NavLink} to="/policies">
<ListItemIcon>
<PermissionIcon />
</ListItemIcon>
<ListItemText primary="IAM Policies" />
</ListItem>
<ListItem button component={NavLink} to="/trace">
<ListItemIcon>
<LoopIcon />
</ListItemIcon>
<ListItemText primary="Trace" />
</ListItem>
<ListItem component={Typography}>Configuration</ListItem>
<ListItem button component={NavLink} to="/notification-endpoints">
<ListItemIcon>
<NotificationsIcon />
</ListItemIcon>
<ListItemText primary="Lambda Notifications" />
</ListItem>
<ListItem button component={NavLink} to="/configurations-list">
<ListItemIcon>
<ListAltIcon />
</ListItemIcon>
<ListItemText primary="Configurations List" />
</ListItem>
<Divider />
<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

@@ -39,27 +39,27 @@ import {
notifyElasticsearch,
notifyWebhooks,
notifyNsq,
removeEmptyFields
removeEmptyFields,
} from "../Configurations/utils";
import { IElementValue } from "../Configurations/types";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
buttonContainer: {
textAlign: "right"
textAlign: "right",
},
logoButton: {
height: "80px"
}
height: "80px",
},
});
interface IAddNotificationEndpointProps {
@@ -73,7 +73,7 @@ const AddNotificationEndpoint = ({
open,
closeModalAndRefresh,
serverNeedsRestart,
classes
classes,
}: IAddNotificationEndpointProps) => {
//Local States
const [service, setService] = useState<string>("");
@@ -86,18 +86,18 @@ const AddNotificationEndpoint = ({
useEffect(() => {
if (saving) {
const payload = {
key_values: removeEmptyFields(valuesArr)
key_values: removeEmptyFields(valuesArr),
};
api
.invoke("PUT", `/api/v1/configs/${service}`, payload)
.then(res => {
.then((res) => {
setSaving(false);
setError("");
serverNeedsRestart(true);
closeModalAndRefresh();
})
.catch(err => {
.catch((err) => {
setSaving(false);
setError(err);
});
@@ -111,7 +111,7 @@ const AddNotificationEndpoint = ({
};
const onValueChange = useCallback(
newValue => {
(newValue) => {
setValueArr(newValue);
},
[setValueArr]
@@ -342,14 +342,14 @@ const AddNotificationEndpoint = ({
</Grid>
)}
<form noValidate onSubmit={submitForm}>
{srvComponent}
<Grid item xs={3} className={classes.buttonContainer}>
<Grid item xs={12}>
{srvComponent}
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={saving}
>
Save

View File

@@ -31,14 +31,14 @@ import {
Radio,
RadioGroup,
Select,
TextField
TextField,
} from "@material-ui/core";
import {
createStyles,
lighten,
makeStyles,
Theme,
withStyles
withStyles,
} from "@material-ui/core/styles";
import api from "../../../common/api";
import clsx from "clsx";
@@ -61,21 +61,21 @@ const useToolbarStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1)
paddingRight: theme.spacing(1),
},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark
backgroundColor: theme.palette.secondary.dark,
},
title: {
flex: "1 1 100%"
}
flex: "1 1 100%",
},
})
);
@@ -90,7 +90,7 @@ const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
return (
<Toolbar
className={clsx(classes.root, {
[classes.highlight]: numSelected > 0
[classes.highlight]: numSelected > 0,
})}
>
{numSelected > 0 ? (
@@ -122,8 +122,8 @@ const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
});
interface IAddPermissionContentProps {
@@ -164,7 +164,7 @@ class AddPermissionContent extends React.Component<
resources: [],
buckets: [],
bucketsError: "",
loadingBuckets: false
loadingBuckets: false,
};
componentDidMount(): void {
@@ -176,10 +176,10 @@ class AddPermissionContent extends React.Component<
this.setState({
loadingBuckets: false,
buckets: res.buckets,
bucketsError: ""
bucketsError: "",
});
})
.catch(err => {
.catch((err) => {
this.setState({ loadingBuckets: false, bucketsError: err });
});
});
@@ -190,8 +190,8 @@ class AddPermissionContent extends React.Component<
name: selectedPermission.name,
description: selectedPermission.description,
effect: selectedPermission.effect,
resources: selectedPermission.resources.map(r => r.bucket_name),
action: selectedPermission.actions[0].type
resources: selectedPermission.resources.map((r) => r.bucket_name),
action: selectedPermission.actions[0].type,
});
}
}
@@ -204,7 +204,7 @@ class AddPermissionContent extends React.Component<
resources,
description,
effect,
action
action,
} = this.state;
const { selectedPermission } = this.props;
if (addLoading) {
@@ -219,23 +219,23 @@ class AddPermissionContent extends React.Component<
description: description,
effect: effect,
resources: resources,
actions: [action]
actions: [action],
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
} else {
@@ -245,23 +245,23 @@ class AddPermissionContent extends React.Component<
description: description,
effect: effect,
resources: resources,
actions: [action]
actions: [action],
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
}
@@ -280,14 +280,14 @@ class AddPermissionContent extends React.Component<
name,
description,
effect,
action
action,
} = this.state;
const handleSelectAllClick = (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (event.target.checked) {
const newSelecteds = buckets.map(n => n.name);
const newSelecteds = buckets.map((n) => n.name);
this.setState({ resources: newSelecteds });
return;
}
@@ -432,7 +432,7 @@ class AddPermissionContent extends React.Component<
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts"
"aria-label": "select all desserts",
}}
/>
</TableCell>
@@ -452,7 +452,9 @@ class AddPermissionContent extends React.Component<
return (
<TableRow
hover
onClick={event => handleClick(event, row.name)}
onClick={(event) =>
handleClick(event, row.name)
}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
@@ -500,7 +502,7 @@ class AddPermissionContent extends React.Component<
value={action}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
action: (event.target as HTMLInputElement).value
action: (event.target as HTMLInputElement).value,
});
}}
>

View File

@@ -24,6 +24,7 @@ import api from "../../../common/api";
import "codemirror/lib/codemirror.css";
import "codemirror/theme/material.css";
import { Policy } from "./types";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
@@ -32,18 +33,19 @@ require("codemirror/mode/javascript/javascript");
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
jsonPolicyEditor: {
minHeight: 400,
width: "100%"
width: "100%",
},
codeMirror: {
fontSize: 14
fontSize: 14,
},
buttonContainer: {
textAlign: "right"
}
textAlign: "right",
},
...modalBasic,
});
interface IAddPolicyProps {
@@ -65,7 +67,7 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
addLoading: false,
addError: "",
policyName: "",
policyDefinition: ""
policyDefinition: "",
};
addRecord(event: React.FormEvent) {
@@ -78,23 +80,23 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
api
.invoke("POST", "/api/v1/policies", {
name: policyName,
policy: policyDefinition
policy: policyDefinition,
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
});
@@ -105,7 +107,7 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
if (policyEdit) {
this.setState({
policyName: policyEdit.name
policyName: policyEdit.name,
});
}
}
@@ -131,51 +133,50 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
<InputBoxWrapper
id="policy-name"
name="policy-name"
label="Policy Name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ policyName: e.target.value });
}}
value={policyName}
disabled={!!policyEdit}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<CodeMirror
className={classes.codeMirror}
value={
policyEdit
? JSON.stringify(JSON.parse(policyEdit.policy), null, 4)
: ""
}
options={{
mode: "javascript",
lineNumbers: true,
}}
onChange={(editor, data, value) => {
this.setState({ policyDefinition: value });
}}
/>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="policy-name"
name="policy-name"
label="Policy Name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ policyName: e.target.value });
}}
value={policyName}
disabled={!!policyEdit}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<CodeMirror
className={classes.codeMirror}
value={
policyEdit
? JSON.stringify(JSON.parse(policyEdit.policy), null, 4)
: ""
}
options={{
mode: "javascript",
lineNumbers: true
}}
onChange={(editor, data, value) => {
this.setState({ policyDefinition: value });
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{!policyEdit && (
<Grid item xs={12} className={classes.buttonContainer}>

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

@@ -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,363 +14,101 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import React, { useEffect, useState } from "react";
import Grid from "@material-ui/core/Grid";
import { UnControlled as CodeMirror } from "react-codemirror2";
import Typography from "@material-ui/core/Typography";
import { Button, FormControlLabel, LinearProgress } from "@material-ui/core";
import {
createStyles,
lighten,
makeStyles,
Theme,
withStyles
} from "@material-ui/core/styles";
import api from "../../../common/api";
import clsx from "clsx";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import Toolbar from "@material-ui/core/Toolbar";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";
import FilterListIcon from "@material-ui/icons/FilterList";
import { Permission, PermissionList } from "../Permissions/types";
import {
NewServiceAccount,
ServiceAccount,
ServiceAccountDetails
} from "./types";
import Switch from "@material-ui/core/Switch";
import { Button, LinearProgress, Tooltip } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import api from "../../../common/api";
import "codemirror/lib/codemirror.css";
import "codemirror/theme/material.css";
import { NewServiceAccount } from "./types";
import HelpIcon from "@material-ui/icons/Help";
const useToolbarStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1)
},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark
},
title: {
flex: "1 1 100%"
}
})
);
interface EnhancedTableToolbarProps {
numSelected: number;
}
const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
const classes = useToolbarStyles();
const { numSelected } = props;
return (
<Toolbar
className={clsx(classes.root, {
[classes.highlight]: numSelected > 0
})}
>
{numSelected > 0 ? (
<Typography
className={classes.title}
color="inherit"
variant="subtitle1"
>
{numSelected} selected
</Typography>
) : (
<Typography className={classes.title} variant="h6" id="tableTitle">
Permissions
</Typography>
)}
{numSelected > 0 ? (
<span />
) : (
<Tooltip title="Filter list">
<IconButton aria-label="filter list">
<FilterListIcon />
</IconButton>
</Tooltip>
)}
</Toolbar>
);
};
require("codemirror/mode/javascript/javascript");
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
jsonPolicyEditor: {
minHeight: 400,
width: "100%",
},
codeMirror: {
fontSize: 14,
},
buttonContainer: {
textAlign: "right",
},
...modalBasic,
});
interface IAddServiceAccountContentProps {
interface IAddServiceAccountProps {
classes: any;
open: boolean;
closeModalAndRefresh: (res: NewServiceAccount | null) => void;
selectedServiceAccount: ServiceAccount | null;
}
interface IAddServiceAccountContentState {
addLoading: boolean;
addError: string;
name: string;
enabled: boolean;
selectedPermissions: Permission[];
rowsPerPage: number;
page: number;
permissions: Permission[];
permissionsError: string;
loadingPermissions: boolean;
loadingServiceAccount: boolean;
}
const AddServiceAccount = ({
classes,
open,
closeModalAndRefresh,
}: IAddServiceAccountProps) => {
const [addSending, setAddSending] = useState(false);
const [addError, setAddError] = useState("");
const [policyDefinition, setPolicyDefinition] = useState("");
class AddServiceAccountContent extends React.Component<
IAddServiceAccountContentProps,
IAddServiceAccountContentState
> {
state: IAddServiceAccountContentState = {
addLoading: false,
addError: "",
name: "",
enabled: true,
selectedPermissions: [],
rowsPerPage: 5,
page: 0,
permissions: [],
permissionsError: "",
loadingPermissions: false,
loadingServiceAccount: false
useEffect(() => {
if (addSending) {
api
.invoke("POST", "/api/v1/service-accounts", {
policy: policyDefinition,
})
.then((res) => {
setAddSending(false);
setAddError("");
closeModalAndRefresh(res);
})
.catch((err) => {
setAddSending(false);
setAddError(err);
});
}
}, [
addSending,
setAddSending,
setAddError,
policyDefinition,
closeModalAndRefresh,
]);
const addServiceAccount = (e: React.FormEvent) => {
e.preventDefault();
setAddSending(true);
};
componentDidMount(): void {
// load a list of permissions
this.setState({ loadingPermissions: true }, () => {
api
.invoke("GET", `/api/v1/permissions?limit=1000`)
.then((res: PermissionList) => {
this.setState({
loadingPermissions: false,
permissions: res.permissions,
permissionsError: ""
});
})
.catch(err => {
this.setState({ loadingPermissions: false, permissionsError: err });
});
});
const { selectedServiceAccount } = this.props;
if (selectedServiceAccount !== null) {
this.setState({ loadingServiceAccount: true }, () => {
api
.invoke(
"GET",
`/api/v1/service_accounts/${selectedServiceAccount.id}`
)
.then((res: ServiceAccountDetails) => {
console.log(res);
this.setState({
loadingServiceAccount: false,
name: selectedServiceAccount.name,
enabled: selectedServiceAccount.enabled,
selectedPermissions:
res.permissions === undefined || res.permissions === null
? []
: res.permissions
});
})
.catch(err => {
this.setState({ loadingServiceAccount: false });
});
});
}
}
saveRecord(event: React.FormEvent) {
event.preventDefault();
const { name, addLoading, selectedPermissions, enabled } = this.state;
const { selectedServiceAccount } = this.props;
if (addLoading) {
return;
}
this.setState({ addLoading: true }, () => {
if (selectedServiceAccount !== null) {
api
.invoke(
"PUT",
`/api/v1/service_accounts/${selectedServiceAccount.id}`,
{
id: selectedServiceAccount.id,
name: name,
enabled: enabled,
permission_ids: selectedPermissions.map(p => p.id)
}
)
.then(res => {
this.setState(
{
addLoading: false,
addError: ""
},
() => {
this.props.closeModalAndRefresh(null);
}
);
})
.catch(err => {
this.setState({
addLoading: false,
addError: err
});
});
} else {
api
.invoke("POST", "/api/v1/service_accounts", {
name: name,
permission_ids: selectedPermissions.map(p => p.id)
})
.then((res: NewServiceAccount) => {
this.setState(
{
addLoading: false,
addError: ""
},
() => {
this.props.closeModalAndRefresh(res);
}
);
})
.catch(err => {
this.setState({
addLoading: false,
addError: err
});
});
}
});
}
render() {
const {
classes,
selectedServiceAccount,
open,
closeModalAndRefresh
} = this.props;
const {
addLoading,
addError,
page,
rowsPerPage,
permissions,
selectedPermissions,
name,
loadingServiceAccount
} = this.state;
const handleSelectAllClick = (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (event.target.checked) {
// const newSelecteds = permissions.map(n => n.name);
const newSelecteds = [...permissions];
this.setState({ selectedPermissions: newSelecteds });
return;
}
this.setState({ selectedPermissions: [] });
};
const handleClick = (
event: React.MouseEvent<unknown>,
perm: Permission
) => {
let newSelected: Permission[] = [...selectedPermissions];
if (newSelected.filter(p => p.id === perm.id).length === 0) {
newSelected.push(perm);
} else {
let selectedIndex = -1;
for (let i = 0; i < newSelected.length; i++) {
if (newSelected[i].id === perm.id) {
selectedIndex = i;
break;
}
}
if (selectedIndex >= 0) {
newSelected = [
...newSelected.slice(0, selectedIndex),
...newSelected.slice(selectedIndex + 1)
];
}
}
this.setState({ selectedPermissions: newSelected });
};
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage });
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({ page: 0, rowsPerPage: parseInt(event.target.value, 10) });
};
const isSelected = (perm: Permission) =>
selectedPermissions.filter(p => p.id === perm.id).length > 0;
const emptyRows =
rowsPerPage -
Math.min(rowsPerPage, permissions.length - page * rowsPerPage);
const handleChange = (name: string) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({ enabled: event.target.checked });
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
this.setState({ addError: "" }, () => {
closeModalAndRefresh(null);
});
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh(null);
}}
title={`Create Service Account`}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
addServiceAccount(e);
}}
title={
selectedServiceAccount !== null
? "Edit Service Account"
: "Create Service Account"
}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
this.saveRecord(e);
}}
>
<Grid container>
{loadingServiceAccount && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
@@ -383,155 +121,46 @@ class AddServiceAccountContent extends React.Component<
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="service-account-name"
name="service-account-name"
label="Name"
value={name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ name: e.target.value });
<Typography component="h5">
Optional Policy
<Tooltip
title="A policy that restricts this service account can be attached."
placement="top-start"
>
<HelpIcon />
</Tooltip>
</Typography>
<CodeMirror
className={classes.codeMirror}
options={{
mode: "javascript",
lineNumbers: true,
}}
onChange={(editor, data, value) => {
setPolicyDefinition(value);
}}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.root}>
<EnhancedTableToolbar
numSelected={selectedPermissions.length}
/>
<TableContainer>
<Table
className={classes.table}
aria-labelledby="tableTitle"
size={"small"}
aria-label="enhanced table"
>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selectedPermissions.length > 0 &&
selectedPermissions.length < permissions.length
}
checked={
selectedPermissions.length > 0 &&
selectedPermissions.length === permissions.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts"
}}
/>
</TableCell>
<TableCell>Permission</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{permissions
.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
)
.map((row, index) => {
const isItemSelected = isSelected(row);
const labelId = `enhanced-table-checkbox-${index}`;
return (
<TableRow
hover
onClick={event => handleClick(event, row)}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
key={row.name}
selected={isItemSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isItemSelected}
inputProps={{ "aria-labelledby": labelId }}
/>
</TableCell>
<TableCell id={labelId}>{row.name}</TableCell>
<TableCell>{row.description}</TableCell>
</TableRow>
);
})}
{emptyRows > 0 && (
<TableRow style={{ height: 33 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={permissions.length}
rowsPerPage={rowsPerPage}
page={page}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
/>
</div>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch onChange={handleChange("enabled")} value="checkedA" />
}
label="Enabled"
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
}
}
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Create
</Button>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
const AddServiceAccountWrapper = withStyles(styles)(AddServiceAccountContent);
interface IAddServiceAccountProps {
open: boolean;
closeModalAndRefresh: (res: NewServiceAccount | null) => void;
selectedServiceAccount: ServiceAccount | null;
}
interface IAddServiceAccountState {}
class AddServiceAccount extends React.Component<
IAddServiceAccountProps,
IAddServiceAccountState
> {
state: IAddServiceAccountState = {};
render() {
return <AddServiceAccountWrapper {...this.props} />;
}
}
export default AddServiceAccount;
export default withStyles(styles)(AddServiceAccount);

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
@@ -17,20 +17,19 @@
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { NewServiceAccount } from "./types";
import {
Button,
DialogActions,
DialogContent,
DialogContentText
} from "@material-ui/core";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import Grid from "@material-ui/core/Grid";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
buttonContainer: {
textAlign: "right",
},
});
interface ICredentialsPromptProps {
@@ -40,76 +39,69 @@ interface ICredentialsPromptProps {
closeModal: () => void;
}
interface ICredentialsPromptState {}
const download = (filename: string, text: string) => {
let element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
);
element.setAttribute("download", filename);
class CredentialsPrompt extends React.Component<
ICredentialsPromptProps,
ICredentialsPromptState
> {
state: ICredentialsPromptState = {};
element.style.display = "none";
document.body.appendChild(element);
download(filename: string, text: string) {
var element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
);
element.setAttribute("download", filename);
element.click();
element.style.display = "none";
document.body.appendChild(element);
document.body.removeChild(element);
};
element.click();
document.body.removeChild(element);
const CredentialsPrompt = ({
classes,
newServiceAccount,
open,
closeModal,
}: ICredentialsPromptProps) => {
if (!newServiceAccount) {
return null;
}
render() {
const { classes, open, newServiceAccount } = this.props;
if (newServiceAccount === null) {
return <div />;
}
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
this.props.closeModal();
}}
title="New Service Account"
>
<React.Fragment>
<DialogContent>
<DialogContentText id="alert-dialog-description">
A new service account has been created with the following details:
<ul>
<li>
<b>Access Key:</b>{" "}
{newServiceAccount.service_account.access_key}
</li>
<li>
<b>Secret Key:</b> {newServiceAccount.secret_key}
</li>
</ul>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Write these down, as this is the only time the secret will be
displayed.
</Typography>
</DialogContentText>
</DialogContent>
<DialogActions>
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModal();
}}
title="New Service Account Created"
>
<React.Fragment>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
A new service account has been created with the following details:
<ul>
<li>
<b>Access Key:</b> {newServiceAccount.accessKey}
</li>
<li>
<b>Secret Key:</b> {newServiceAccount.secretKey}
</li>
</ul>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Write these down, as this is the only time the secret will be
displayed.
</Typography>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
onClick={() => {
this.download(
download(
"credentials.json",
JSON.stringify({
access_key: newServiceAccount.service_account.access_key,
secret_key: newServiceAccount.secret_key
access_key: newServiceAccount.accessKey,
secret_key: newServiceAccount.secretKey,
})
);
}}
@@ -119,18 +111,18 @@ class CredentialsPrompt extends React.Component<
</Button>
<Button
onClick={() => {
this.props.closeModal();
closeModal();
}}
color="secondary"
autoFocus
>
Done
</Button>
</DialogActions>
</React.Fragment>
</ModalWrapper>
);
}
}
</Grid>
</Grid>
</React.Fragment>
</ModalWrapper>
);
};
export default withStyles(styles)(CredentialsPrompt);

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,152 +14,117 @@
// 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 React from "react";
import Typography from "@material-ui/core/Typography";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@material-ui/core";
import api from "../../../common/api";
import { ServiceAccount, ServiceAccountsList } from "./types";
import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
},
wrapText: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
}
});
createStyles({
errorBlock: {
color: "red",
},
wrapText: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word",
},
});
interface IDeleteServiceAccountProps {
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedServiceAccount: ServiceAccount | null;
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedServiceAccount: string | null;
}
interface IDeleteServiceAccountState {
deleteLoading: boolean;
deleteError: string;
}
const DeleteServiceAccount = ({
classes,
closeDeleteModalAndRefresh,
deleteOpen,
selectedServiceAccount,
}: IDeleteServiceAccountProps) => {
const [deleteLoading, setDeleteLoading] = useState(false);
const [deleteError, setDeleteError] = useState("");
class DeleteServiceAccount extends React.Component<
IDeleteServiceAccountProps,
IDeleteServiceAccountState
> {
state: IDeleteServiceAccountState = {
deleteLoading: false,
deleteError: ""
};
removeRecord() {
const { deleteLoading } = this.state;
const { selectedServiceAccount } = this.props;
if (deleteLoading) {
return;
}
if (selectedServiceAccount == null) {
return;
}
this.setState({ deleteLoading: true }, () => {
api
.invoke("DELETE", `/api/v1/service_accounts/${selectedServiceAccount.id}`, {
id: selectedServiceAccount.id
})
.then((res: ServiceAccountsList) => {
this.setState(
{
deleteLoading: false,
deleteError: ""
},
() => {
this.props.closeDeleteModalAndRefresh(true);
}
);
})
.catch(err => {
this.setState({
deleteLoading: false,
deleteError: err
});
});
useEffect(() => {
if (deleteLoading) {
api
.invoke("DELETE", `/api/v1/service-accounts/${selectedServiceAccount}`)
.then(() => {
setDeleteLoading(false);
setDeleteError("");
closeDeleteModalAndRefresh(true);
})
.catch((err) => {
setDeleteLoading(false);
setDeleteError(err);
});
}
}, [deleteLoading, closeDeleteModalAndRefresh, selectedServiceAccount]);
render() {
const { classes, deleteOpen, selectedServiceAccount } = this.props;
const { deleteLoading, deleteError } = this.state;
if (selectedServiceAccount === null) {
return <div />;
}
return (
<Dialog
open={deleteOpen}
onClose={() => {
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete ServiceAccount</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete service account{" "}
<b className={classes.wrapText}>{selectedServiceAccount.name}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
this.removeRecord();
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
);
const removeRecord = () => {
if (selectedServiceAccount == null) {
return;
}
}
setDeleteLoading(true);
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete ServiceAccount</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete service account{" "}
<b className={classes.wrapText}>{selectedServiceAccount}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button onClick={removeRecord} color="secondary" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
);
};
export default withStyles(styles)(DeleteServiceAccount);

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,401 +14,295 @@
// 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import api from "../../../common/api";
import {
Button,
IconButton,
LinearProgress,
TableFooter,
TablePagination
} from "@material-ui/core";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import {
NewServiceAccount,
ServiceAccount,
ServiceAccountsList
} from "./types";
import { NewServiceAccount } from "./types";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import EditIcon from "@material-ui/icons/Edit";
import AddServiceAccount from "./AddServiceAccount";
import DeleteServiceAccount from "./DeleteServiceAccount";
import CredentialsPrompt from "./CredentialsPrompt";
import { CreateIcon, DeleteIcon } from "../../../icons";
import Checkbox from "@material-ui/core/Checkbox";
import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded";
import { CreateIcon } from "../../../icons";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import { stringSort } from "../../../utils/sortFunctions";
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: "480px",
minWidth: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
imageIcon: {
height: "100%"
height: "100%",
},
iconRoot: {
textAlign: "center"
textAlign: "center",
},
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 IServiceAccountsProps {
classes: any;
}
interface IServiceAccountsState {
records: ServiceAccount[];
totalRecords: number;
loading: boolean;
error: string;
deleteError: string;
addScreenOpen: boolean;
page: number;
rowsPerPage: number;
deleteOpen: boolean;
selectedServiceAccount: ServiceAccount | null;
showNewCredentials: boolean;
newServiceAccount: NewServiceAccount | null;
}
const ServiceAccounts = ({ classes }: IServiceAccountsProps) => {
const [records, setRecords] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [filter, setFilter] = 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 [selectedServiceAccount, setSelectedServiceAccount] = useState<
string | null
>(null);
const [showNewCredentials, setShowNewCredentials] = useState<boolean>(false);
const [
newServiceAccount,
setNewServiceAccount,
] = useState<NewServiceAccount | null>(null);
class ServiceAccounts extends React.Component<
IServiceAccountsProps,
IServiceAccountsState
> {
state: IServiceAccountsState = {
records: [],
totalRecords: 0,
loading: false,
error: "",
deleteError: "",
addScreenOpen: false,
page: 0,
rowsPerPage: 10,
deleteOpen: false,
selectedServiceAccount: null,
showNewCredentials: false,
newServiceAccount: null
};
useEffect(() => {
fetchRecords();
}, []);
fetchRecords() {
this.setState({ loading: true }, () => {
const { page, rowsPerPage } = this.state;
const offset = page * rowsPerPage;
useEffect(() => {
if (loading) {
api
.invoke(
"GET",
`/api/v1/service_accounts?offset=${offset}&limit=${rowsPerPage}`
)
.then((res: ServiceAccountsList) => {
this.setState({
loading: false,
records: res.service_accounts,
totalRecords: res.total,
error: ""
});
.invoke("GET", `/api/v1/service-accounts`)
.then((res: string[]) => {
const serviceAccounts = res.sort(stringSort);
setLoading(false);
setRecords(serviceAccounts);
setError("");
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.service_accounts === undefined ||
res.service_accounts == null ||
res.service_accounts.length === 0) &&
page > 0
) {
if ((!serviceAccounts || serviceAccounts.length === 0) && 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) => {
setError(err);
setLoading(false);
});
});
}
closeAddModalAndRefresh(res: NewServiceAccount | null) {
this.setState({ addScreenOpen: false }, () => {
this.fetchRecords();
});
if (res !== null) {
this.setState({ showNewCredentials: true, newServiceAccount: res });
}
}
}, [loading, setLoading, setRecords, setError, page, setPage]);
closeDeleteModalAndRefresh(refresh: boolean) {
this.setState({ deleteOpen: false }, () => {
if (refresh) {
this.fetchRecords();
}
});
}
const fetchRecords = () => {
setLoading(true);
};
componentDidMount(): void {
this.fetchRecords();
}
const closeAddModalAndRefresh = (res: NewServiceAccount | null) => {
setAddScreenOpen(false);
fetchRecords();
closeCredentialsModal() {
this.setState({ showNewCredentials: false, newServiceAccount: null });
}
if (res !== null) {
setShowNewCredentials(true);
setNewServiceAccount(res);
}
};
render() {
const { classes } = this.props;
const {
records,
totalRecords,
addScreenOpen,
loading,
page,
rowsPerPage,
deleteOpen,
selectedServiceAccount,
showNewCredentials,
newServiceAccount
} = this.state;
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
};
if (refresh) {
fetchRecords();
}
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
this.setState({ page: 0, rowsPerPage: rPP }, () => {
this.fetchRecords();
});
};
const closeCredentialsModal = () => {
setShowNewCredentials(false);
setNewServiceAccount(null);
};
const confirmDeleteServiceAccount = (
selectedServiceAccount: ServiceAccount
) => {
this.setState({
deleteOpen: true,
selectedServiceAccount: selectedServiceAccount
});
};
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const editServiceAccount = (selectedServiceAccount: ServiceAccount) => {
this.setState({
addScreenOpen: true,
selectedServiceAccount: selectedServiceAccount
});
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
return (
<React.Fragment>
{addScreenOpen && (
<AddServiceAccount
open={addScreenOpen}
selectedServiceAccount={selectedServiceAccount}
closeModalAndRefresh={(res: NewServiceAccount | null) => {
this.closeAddModalAndRefresh(res);
}}
/>
)}
{deleteOpen && (
<DeleteServiceAccount
deleteOpen={deleteOpen}
selectedServiceAccount={selectedServiceAccount}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
)}
{showNewCredentials && (
<CredentialsPrompt
newServiceAccount={newServiceAccount}
open={showNewCredentials}
closeModal={() => {
this.closeCredentialsModal();
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Service Accounts</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Service Accounts"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
this.setState({
addScreenOpen: true,
selectedServiceAccount: null
});
}}
>
Create service account
</Button>
<Button
variant="contained"
color="primary"
startIcon={<PlayArrowRoundedIcon />}
onClick={() => {
this.setState({
addScreenOpen: true
});
}}
>
Change Access
</Button>
</Grid>
setPage(0);
setRowsPerPage(rPP);
};
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
{records != null && records.length > 0 ? (
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Name</TableCell>
<TableCell>Access Key</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map(row => (
<TableRow key={row.name}>
<TableCell padding="checkbox">
<Checkbox
value="secondary"
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.name}
</TableCell>
<TableCell>{row.access_key}</TableCell>
<TableCell>
{row.enabled ? "enabled" : "disabled"}
</TableCell>
<TableCell align="right">
<IconButton
aria-label="edit"
onClick={() => {
editServiceAccount(row);
}}
>
<EditIcon />
</IconButton>
<IconButton
aria-label="delete"
onClick={() => {
confirmDeleteServiceAccount(row);
}}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
colSpan={4}
count={totalRecords}
rowsPerPage={rowsPerPage}
page={page}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true
}}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
ActionsComponent={MinTablePaginationActions}
/>
</TableRow>
</TableFooter>
</Table>
) : (
<div>No Service Accounts</div>
)}
</Paper>
</Grid>
const confirmDeleteServiceAccount = (selectedServiceAccount: string) => {
setSelectedServiceAccount(selectedServiceAccount);
setDeleteOpen(true);
};
const tableActions = [
{ type: "delete", onClick: confirmDeleteServiceAccount },
];
const filteredRecords = records.filter((elementItem) =>
elementItem.toLowerCase().includes(filter.toLowerCase())
);
const beginRecord = page * rowsPerPage;
const endRecords = beginRecord + rowsPerPage;
const paginatedRecords = filteredRecords.slice(beginRecord, endRecords);
return (
<React.Fragment>
{addScreenOpen && (
<AddServiceAccount
open={addScreenOpen}
closeModalAndRefresh={(res: NewServiceAccount | null) => {
closeAddModalAndRefresh(res);
}}
/>
)}
{deleteOpen && (
<DeleteServiceAccount
deleteOpen={deleteOpen}
selectedServiceAccount={selectedServiceAccount}
closeDeleteModalAndRefresh={(refresh: boolean) => {
closeDeleteModalAndRefresh(refresh);
}}
/>
)}
{showNewCredentials && (
<CredentialsPrompt
newServiceAccount={newServiceAccount}
open={showNewCredentials}
closeModal={() => {
closeCredentialsModal();
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Service Accounts</Typography>
</Grid>
</React.Fragment>
);
}
}
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Service Accounts"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
setPage(0);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
setSelectedServiceAccount(null);
}}
>
Create service account
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{error !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{error}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<TableWrapper
isLoading={loading}
records={paginatedRecords}
entityName={"Service Accounts"}
idField={""}
columns={[{ label: "Service Account", elementKey: "" }]}
itemActions={tableActions}
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 4,
count: records.length,
rowsPerPage: rowsPerPage,
page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ServiceAccounts);

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,12 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import {Permission} from "../Permissions/types";
export interface ServiceAccount {
id: string;
name: string;
slug: string;
access_key: string;
enabled: boolean;
}
export interface ServiceAccountsList {
service_accounts: ServiceAccount[];
total:number;
service_accounts: string[];
total: number;
}
export interface NewServiceAccount {
service_account: ServiceAccount,
secret_key:string,
}
export interface ServiceAccountDetails {
service_account: ServiceAccount,
permissions: Permission[],
accessKey: string;
secretKey: string;
}

View File

@@ -0,0 +1,370 @@
// 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 { Button, LinearProgress } from "@material-ui/core";
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";
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,
},
...modalBasic,
});
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>(false);
const [enableSSL, setEnableSSL] = useState<boolean>(false);
const [sizeFactor, setSizeFactor] = useState<string>("Gi");
const [storageClasses, setStorageClassesList] = useState<string[]>([]);
useEffect(() => {
fetchStorageClassList();
}, []);
useEffect(() => {
if (addSending) {
let cleanZones: IZone[] = [];
for (let zone of zones) {
if (zone.name !== "") {
cleanZones.push(zone);
}
}
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);
});
}
}, [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);
})
.catch((err: any) => {
console.log(err);
});
};
const storageClassesList = storageClasses.map((s: string) => ({
label: s,
value: s,
}));
return (
<ModalWrapper
title="Create Tenant"
modalOpen={open}
onClose={() => {
setAddError("");
closeModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddSending(true);
}}
>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="tenant-name"
name="tenant-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTenantName(e.target.value);
}}
label="Tenant Name"
value={tenantName}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="image"
name="image"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setImageName(e.target.value);
}}
label="MinIO Image"
value={imageName}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="service_name"
name="service_name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setServiceName(e.target.value);
}}
label="Service Name"
value={serviceName}
/>
</Grid>
<Grid item xs={12}>
<div>
<ZonesMultiSelector
label="Zones"
name="zones_selector"
onChange={(elements: IZone[]) => {
setZones(elements);
}}
elements={zones}
/>
</div>
</Grid>
<Grid item xs={12}>
<Typography component="h3">Volume Configuration</Typography>
</Grid>
<Grid item xs={12}>
<br />
</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));
}}
label="Volumes per Server"
value={volumesPerServer.toString(10)}
/>
</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);
}}
label="Size"
value={volumeConfiguration.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>
<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={storageClassesList}
/>
</Grid>
<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="access_key"
name="access_key"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
}}
label="Access Key"
value={accessKey}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="secret_key"
name="secret_key"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
}}
label="Secret Key"
value={secretKey}
/>
</Grid>
<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>
</Grid>
<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>
)}
</Grid>
</form>
</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);

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