Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35c3b53a23 | ||
|
|
6fef30f29d | ||
|
|
acf480fd25 | ||
|
|
8bbc4f0192 | ||
|
|
48e7991f11 | ||
|
|
5e9b0652b0 | ||
|
|
438211199d | ||
|
|
0f77a32656 | ||
|
|
9f3e99ede8 | ||
|
|
a8c07c0969 |
127
DEVELOPMENT.md
Normal file
127
DEVELOPMENT.md
Normal 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
|
||||
```
|
||||
@@ -68,6 +68,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
|
||||
|
||||
4
go.mod
4
go.mod
@@ -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/mc v0.0.0-20200515191050-09c31c4ab28c
|
||||
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0
|
||||
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22
|
||||
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
|
||||
|
||||
12
go.sum
12
go.sum
@@ -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,7 +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/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=
|
||||
@@ -41,7 +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/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=
|
||||
@@ -114,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=
|
||||
@@ -399,15 +399,17 @@ 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/go.mod h1:v8oQPMMaTkjDwp5cOz1WCElA4Ik+X+0y4On+VMk0fis=
|
||||
github.com/minio/mc v0.0.0-20200515191050-09c31c4ab28c h1:G4ZTNwiiJ73folxqNXZpWQofxus2fGJG7hKxYNrtvRM=
|
||||
github.com/minio/mc v0.0.0-20200515191050-09c31c4ab28c/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-20200501193630-d1c8e9f31ba0 h1:QxIz36O01LbKqJiz6HKeKCOC2afgydspkpahQ807msY=
|
||||
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0/go.mod h1:Vhlqz7Se0EgpgFiVxpvzF4Zz/h2LMx+EPKH96Aera5U=
|
||||
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.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=
|
||||
|
||||
39
pkg/auth/ldap.go
Normal file
39
pkg/auth/ldap.go
Normal 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
27
pkg/auth/ldap/config.go
Normal 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
22
pkg/auth/ldap/const.go
Normal 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"
|
||||
)
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -38,3 +38,12 @@ 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()}`;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,8 +26,9 @@ import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapp
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
}
|
||||
color: "red",
|
||||
},
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
interface ISetAccessPolicyProps {
|
||||
@@ -49,7 +51,7 @@ class SetAccessPolicy extends React.Component<
|
||||
state: ISetAccessPolicyState = {
|
||||
addLoading: false,
|
||||
addError: "",
|
||||
accessPolicy: ""
|
||||
accessPolicy: "",
|
||||
};
|
||||
|
||||
addRecord(event: React.FormEvent) {
|
||||
@@ -62,23 +64,23 @@ 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -105,34 +107,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
|
||||
|
||||
@@ -32,50 +32,50 @@ import TableWrapper from "../../Common/TableWrapper/TableWrapper";
|
||||
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",
|
||||
},
|
||||
});
|
||||
|
||||
interface IViewBucketProps {
|
||||
@@ -113,7 +113,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
addScreenOpen: false,
|
||||
deleteOpen: false,
|
||||
selectedBucket: "",
|
||||
selectedEvent: null
|
||||
selectedEvent: null,
|
||||
};
|
||||
|
||||
fetchEvents() {
|
||||
@@ -131,7 +131,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
loading: 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) {
|
||||
@@ -169,7 +169,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
.then((res: BucketInfo) => {
|
||||
this.setState({ info: res });
|
||||
})
|
||||
.catch(err => {});
|
||||
.catch((err) => {});
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -191,7 +191,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
rowsPerPage,
|
||||
deleteOpen,
|
||||
addScreenOpen,
|
||||
selectedEvent
|
||||
selectedEvent,
|
||||
} = this.state;
|
||||
|
||||
const offset = page * rowsPerPage;
|
||||
@@ -228,21 +228,26 @@ 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}
|
||||
closeModalAndRefresh={() => {
|
||||
this.closeAddModalAndRefresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">
|
||||
@@ -261,7 +266,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
setAccessPolicyScreenOpen: true
|
||||
setAccessPolicyScreenOpen: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -285,7 +290,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
startIcon={<CreateIcon />}
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
addScreenOpen: true
|
||||
addScreenOpen: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -303,10 +308,10 @@ 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}
|
||||
records={filteredRecords}
|
||||
@@ -320,11 +325,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>
|
||||
|
||||
@@ -19,7 +19,7 @@ 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 {
|
||||
@@ -34,9 +34,10 @@ interface ICSVMultiSelector {
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...fieldBasic,
|
||||
...tooltipHelper,
|
||||
inputLabel: {
|
||||
...fieldBasic.inputLabel,
|
||||
marginBottom: 10
|
||||
width: 116,
|
||||
},
|
||||
inputContainer: {
|
||||
height: 150,
|
||||
@@ -44,11 +45,10 @@ const styles = (theme: Theme) =>
|
||||
padding: 15,
|
||||
position: "relative",
|
||||
border: "1px solid #c4c4c4",
|
||||
marginBottom: 10
|
||||
},
|
||||
labelContainer: {
|
||||
display: "flex"
|
||||
}
|
||||
display: "flex",
|
||||
},
|
||||
});
|
||||
|
||||
const CSVMultiSelector = ({
|
||||
@@ -57,7 +57,7 @@ const CSVMultiSelector = ({
|
||||
label,
|
||||
tooltip = "",
|
||||
onChange,
|
||||
classes
|
||||
classes,
|
||||
}: ICSVMultiSelector) => {
|
||||
const [currentElements, setCurrentElements] = useState<string[]>([""]);
|
||||
const bottomList = createRef<HTMLDivElement>();
|
||||
@@ -80,7 +80,7 @@ const CSVMultiSelector = ({
|
||||
// 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
|
||||
@@ -129,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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,13 +86,21 @@ 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}
|
||||
<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
|
||||
@@ -94,7 +110,7 @@ const SelectWrapper = ({
|
||||
onChange={onChange}
|
||||
input={<SelectStyled />}
|
||||
>
|
||||
{options.map(option => (
|
||||
{options.map((option) => (
|
||||
<MenuItem
|
||||
value={option.value}
|
||||
key={`select-${name}-${option.label}`}
|
||||
|
||||
@@ -19,17 +19,45 @@
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
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 {
|
||||
@@ -29,7 +30,10 @@ interface IConfGenericProps {
|
||||
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
|
||||
@@ -41,7 +45,7 @@ export const valueDef = (
|
||||
let defValue = type === "on|off" ? "false" : "";
|
||||
|
||||
if (defaults.length > 0) {
|
||||
const storedConfig = defaults.find(element => element.key === key);
|
||||
const storedConfig = defaults.find((element) => element.key === key);
|
||||
|
||||
if (storedConfig) {
|
||||
defValue = storedConfig.value;
|
||||
@@ -55,7 +59,7 @@ const ConfTargetGeneric = ({
|
||||
onChange,
|
||||
fields,
|
||||
defaultVals,
|
||||
classes
|
||||
classes,
|
||||
}: IConfGenericProps) => {
|
||||
const [valueHolder, setValueHolder] = useState<IElementValue[]>([]);
|
||||
const fieldsElements = !fields ? [] : fields;
|
||||
@@ -64,10 +68,10 @@ const ConfTargetGeneric = ({
|
||||
// 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: valueDef(field.name, field.type, defValList)
|
||||
value: valueDef(field.name, field.type, defValList),
|
||||
};
|
||||
values.push(stateInsert);
|
||||
});
|
||||
@@ -102,7 +106,7 @@ const ConfTargetGeneric = ({
|
||||
}
|
||||
selectorOptions={[
|
||||
{ label: "On", value: "true" },
|
||||
{ label: "Off", value: "false" }
|
||||
{ label: "Off", value: "false" },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
@@ -137,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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,35 +16,37 @@
|
||||
|
||||
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, 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 {
|
||||
@@ -60,7 +62,7 @@ const EditConfiguration = ({
|
||||
closeModalAndRefresh,
|
||||
serverNeedsRestart,
|
||||
selectedConfiguration,
|
||||
classes
|
||||
classes,
|
||||
}: IAddNotificationEndpointProps) => {
|
||||
//Local States
|
||||
const [valuesObj, setValueObj] = useState<IElementValue[]>([]);
|
||||
@@ -75,11 +77,11 @@ const EditConfiguration = ({
|
||||
if (configId) {
|
||||
api
|
||||
.invoke("GET", `/api/v1/configs/${configId}`)
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
const keyVals = get(res, "key_values", []);
|
||||
setConfigValues(keyVals);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setLoadingConfig(false);
|
||||
setErrorConfig(err);
|
||||
});
|
||||
@@ -90,7 +92,7 @@ const EditConfiguration = ({
|
||||
useEffect(() => {
|
||||
if (saving) {
|
||||
const payload = {
|
||||
key_values: removeEmptyFields(valuesObj)
|
||||
key_values: removeEmptyFields(valuesObj),
|
||||
};
|
||||
api
|
||||
.invoke(
|
||||
@@ -98,14 +100,14 @@ const EditConfiguration = ({
|
||||
`/api/v1/configs/${selectedConfiguration.configuration_id}`,
|
||||
payload
|
||||
)
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
setSaving(false);
|
||||
setErrorConfig("");
|
||||
serverNeedsRestart(true);
|
||||
|
||||
closeModalAndRefresh();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setSaving(false);
|
||||
setErrorConfig(err);
|
||||
});
|
||||
@@ -115,7 +117,7 @@ const EditConfiguration = ({
|
||||
serverNeedsRestart,
|
||||
selectedConfiguration,
|
||||
valuesObj,
|
||||
closeModalAndRefresh
|
||||
closeModalAndRefresh,
|
||||
]);
|
||||
|
||||
//Fetch Actions
|
||||
@@ -125,7 +127,7 @@ const EditConfiguration = ({
|
||||
};
|
||||
|
||||
const onValueChange = useCallback(
|
||||
newValue => {
|
||||
(newValue) => {
|
||||
setValueObj(newValue);
|
||||
},
|
||||
[setValueObj]
|
||||
@@ -157,12 +159,11 @@ const EditConfiguration = ({
|
||||
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
|
||||
|
||||
@@ -20,7 +20,7 @@ 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";
|
||||
@@ -36,14 +36,14 @@ 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";
|
||||
@@ -63,6 +63,7 @@ import { Button, LinearProgress } from "@material-ui/core";
|
||||
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
|
||||
import Trace from "./Trace/Trace";
|
||||
import Logs from "./Logs/Logs";
|
||||
import Watch from "./Watch/Watch";
|
||||
|
||||
function Copyright() {
|
||||
return (
|
||||
@@ -82,43 +83,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",
|
||||
@@ -126,40 +127,40 @@ 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,
|
||||
}),
|
||||
},
|
||||
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,
|
||||
@@ -167,20 +168,20 @@ 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
|
||||
isServerLoading: state.system.serverIsLoading,
|
||||
});
|
||||
|
||||
const connector = connect(mapState, {
|
||||
setMenuOpen,
|
||||
serverNeedsRestart,
|
||||
serverIsLoading
|
||||
serverIsLoading,
|
||||
});
|
||||
|
||||
interface IConsoleProps {
|
||||
@@ -196,14 +197,14 @@ interface IConsoleProps {
|
||||
|
||||
class Console extends React.Component<
|
||||
IConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps
|
||||
> {
|
||||
> {
|
||||
componentDidMount(): void {
|
||||
api
|
||||
.invoke("GET", `/api/v1/session`)
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
storage.removeItem("token");
|
||||
history.push("/");
|
||||
});
|
||||
@@ -213,13 +214,13 @@ class Console extends React.Component<
|
||||
this.props.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);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.props.serverIsLoading(false);
|
||||
console.log("failure restarting service");
|
||||
console.log(err);
|
||||
@@ -234,7 +235,7 @@ class Console extends React.Component<
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
classes={{
|
||||
paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose)
|
||||
paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose),
|
||||
}}
|
||||
open={open}
|
||||
>
|
||||
@@ -261,20 +262,20 @@ class Console extends React.Component<
|
||||
<LinearProgress />
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
The instance needs to be restarted for configuration changes
|
||||
<React.Fragment>
|
||||
The instance needs to be restarted for configuration changes
|
||||
to take effect.{" "}
|
||||
<Button
|
||||
color="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
this.restartServer();
|
||||
}}
|
||||
>
|
||||
Restart
|
||||
<Button
|
||||
color="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
this.restartServer();
|
||||
}}
|
||||
>
|
||||
Restart
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.appBarSpacer} />
|
||||
@@ -306,6 +307,7 @@ class Console extends React.Component<
|
||||
<Route exact path="/webhook/audit" component={WebhookPanel} />
|
||||
<Route exct path="/trace" component={Trace} />
|
||||
<Route exct path="/logs" component={Logs} />
|
||||
<Route exct path="/watch" component={Watch} />
|
||||
<Route exact path="/">
|
||||
<Redirect to="/dashboard" />
|
||||
</Route>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,15 +15,13 @@
|
||||
// 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 storage from "local-storage-fallback";
|
||||
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 { niceBytes } from "../../../common/utils";
|
||||
import Ansi from "ansi-to-react";
|
||||
import { isNull, isNullOrUndefined } from "util";
|
||||
import { timeFromDate } from "../../../common/utils";
|
||||
import { isNullOrUndefined } from "util";
|
||||
import { wsProtocol } from "../../../utils/wsUtils";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
@@ -34,31 +32,28 @@ const styles = (theme: Theme) =>
|
||||
overflow: "auto",
|
||||
"& ul": {
|
||||
margin: "4px",
|
||||
padding: "0px"
|
||||
padding: "0px",
|
||||
},
|
||||
"& ul li": {
|
||||
listStyle: "none",
|
||||
margin: "0px",
|
||||
padding: "0px",
|
||||
borderBottom: "1px solid #dedede"
|
||||
}
|
||||
borderBottom: "1px solid #dedede",
|
||||
},
|
||||
},
|
||||
tab: {
|
||||
padding: "25px"
|
||||
},
|
||||
ansiblue: {
|
||||
color: "blue"
|
||||
padding: "25px",
|
||||
},
|
||||
logerror: {
|
||||
color: "red"
|
||||
color: "#A52A2A",
|
||||
},
|
||||
logerror_tab: {
|
||||
color: "red",
|
||||
padding: "25px"
|
||||
color: "#A52A2A",
|
||||
padding: "25px",
|
||||
},
|
||||
ansidefault: {
|
||||
color: "black"
|
||||
}
|
||||
color: "black",
|
||||
},
|
||||
});
|
||||
|
||||
interface ILogs {
|
||||
@@ -72,7 +67,7 @@ const Logs = ({
|
||||
classes,
|
||||
logMessageReceived,
|
||||
logResetMessages,
|
||||
messages
|
||||
messages,
|
||||
}: ILogs) => {
|
||||
useEffect(() => {
|
||||
logResetMessages();
|
||||
@@ -114,14 +109,6 @@ const Logs = ({
|
||||
}
|
||||
}, [logMessageReceived]);
|
||||
|
||||
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()}`;
|
||||
};
|
||||
|
||||
// replaces a character of a string with other at a given index
|
||||
const replaceWeirdChar = (
|
||||
origString: string,
|
||||
@@ -135,23 +122,6 @@ const Logs = ({
|
||||
return newString;
|
||||
};
|
||||
|
||||
const colorify = (str: string) => {
|
||||
// matches strings starting like: `[34mEndpoint: [0m`
|
||||
const colorRegex = /(\[[0-9]+m)(.*?)(\[0+m)/g;
|
||||
let matches = colorRegex.exec(str);
|
||||
if (!isNullOrUndefined(matches)) {
|
||||
let start_color = matches[1];
|
||||
let text = matches[2];
|
||||
|
||||
if (start_color === "[34m") {
|
||||
return <span className={classes.ansiblue}>{text}</span>;
|
||||
}
|
||||
if (start_color === "[1m") {
|
||||
return <span className={classes.ansidarkblue}>{text}</span>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderError = (logElement: LogMessage) => {
|
||||
let errorElems = [];
|
||||
if (!isNullOrUndefined(logElement.error)) {
|
||||
@@ -166,7 +136,7 @@ const Logs = ({
|
||||
errorElems.push(
|
||||
<li key={`time-${logElement.key}`}>
|
||||
<span className={classes.logerror}>
|
||||
Time: {timeFromdate(logElement.time)}
|
||||
Time: {timeFromDate(logElement.time)}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
@@ -241,36 +211,22 @@ const Logs = ({
|
||||
|
||||
const renderLog = (logElement: LogMessage) => {
|
||||
let logMessage = logElement.ConsoleMsg;
|
||||
// if somehow after the color code starts with unicode 10 = Line feed
|
||||
// delete it
|
||||
const regexInit = /(\[[0-9]+m)/g;
|
||||
let match = regexInit.exec(logMessage);
|
||||
if (match) {
|
||||
if (logMessage.slice(match[0].length).codePointAt(1) == 10) {
|
||||
logMessage = replaceWeirdChar(logMessage, "", match[0].length + 1);
|
||||
}
|
||||
}
|
||||
// remove any non ascii characters, exclude any control codes
|
||||
logMessage = logMessage.replace(/([^\x20-\x7F])/g, "");
|
||||
|
||||
// Select what to add color and what not to.
|
||||
const colorRegex = /(\[[0-9]+m)(.*?)(\[0+m)/g;
|
||||
let m = colorRegex.exec(logMessage);
|
||||
// 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.slice(colorRegex.lastIndex);
|
||||
substr = substr.replace(regexInit, "");
|
||||
let substr = logMessage.replace(tColorRegex, "");
|
||||
|
||||
// strClean used for corner case when string has unicode 32 for
|
||||
// space instead of normal space.
|
||||
let strClean = logMessage.replace(regexInit, "");
|
||||
// if starts with multiple spaces add padding
|
||||
if (strClean.startsWith(" ") || strClean.codePointAt(1) === 32) {
|
||||
if (substr.startsWith(" ")) {
|
||||
return (
|
||||
<li key={logElement.key}>
|
||||
<span className={classes.tab}>
|
||||
{colorify(logMessage)} {substr}
|
||||
</span>
|
||||
<span className={classes.tab}>{substr}</span>
|
||||
</li>
|
||||
);
|
||||
} else if (!isNullOrUndefined(logElement.error)) {
|
||||
@@ -280,9 +236,7 @@ const Logs = ({
|
||||
// for all remaining set default class
|
||||
return (
|
||||
<li key={logElement.key}>
|
||||
<span className={classes.ansidefault}>
|
||||
{colorify(logMessage)} {substr}
|
||||
</span>
|
||||
<span className={classes.ansidefault}>{substr}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -293,7 +247,7 @@ const Logs = ({
|
||||
<h1>Logs</h1>
|
||||
<div className={classes.logList}>
|
||||
<ul>
|
||||
{messages.map(m => {
|
||||
{messages.map((m) => {
|
||||
return renderLog(m);
|
||||
})}
|
||||
</ul>
|
||||
@@ -303,12 +257,12 @@ const Logs = ({
|
||||
};
|
||||
|
||||
const mapState = (state: AppState) => ({
|
||||
messages: state.logs.messages
|
||||
messages: state.logs.messages,
|
||||
});
|
||||
|
||||
const connector = connect(mapState, {
|
||||
logMessageReceived: logMessageReceived,
|
||||
logResetMessages: logResetMessages
|
||||
logResetMessages: logResetMessages,
|
||||
});
|
||||
|
||||
export default connector(withStyles(styles)(Logs));
|
||||
|
||||
@@ -18,6 +18,7 @@ import React from "react";
|
||||
import ListItem from "@material-ui/core/ListItem";
|
||||
import ListItemIcon from "@material-ui/core/ListItemIcon";
|
||||
import WebAssetIcon from "@material-ui/icons/WebAsset";
|
||||
import CenterFocusWeakIcon from '@material-ui/icons/CenterFocusWeak';
|
||||
import ListItemText from "@material-ui/core/ListItemText";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { Divider, Typography, withStyles } from "@material-ui/core";
|
||||
@@ -33,7 +34,7 @@ import {
|
||||
BucketsIcon,
|
||||
DashboardIcon,
|
||||
PermissionIcon,
|
||||
UsersIcon
|
||||
UsersIcon,
|
||||
} from "../../icons";
|
||||
import { createStyles, Theme } from "@material-ui/core/styles";
|
||||
import PersonIcon from "@material-ui/icons/Person";
|
||||
@@ -49,8 +50,8 @@ const styles = (theme: Theme) =>
|
||||
marginBottom: "20px",
|
||||
textAlign: "center",
|
||||
"& img": {
|
||||
width: "120px"
|
||||
}
|
||||
width: "120px",
|
||||
},
|
||||
},
|
||||
menuList: {
|
||||
"& .active": {
|
||||
@@ -60,31 +61,31 @@ const styles = (theme: Theme) =>
|
||||
background:
|
||||
"transparent linear-gradient(90deg, #362585 0%, #362585 7%, #281B6F 39%, #1F1661 100%) 0% 0% no-repeat padding-box",
|
||||
"& .MuiSvgIcon-root": {
|
||||
color: "white"
|
||||
}
|
||||
color: "white",
|
||||
},
|
||||
},
|
||||
"& .MuiListItem-root": {
|
||||
marginTop: "16px"
|
||||
marginTop: "16px",
|
||||
},
|
||||
paddingLeft: "30px",
|
||||
"& .MuiSvgIcon-root": {
|
||||
fontSize: "18px",
|
||||
color: "#393939"
|
||||
color: "#393939",
|
||||
},
|
||||
"& .MuiListItemIcon-root": {
|
||||
minWidth: "40px"
|
||||
minWidth: "40px",
|
||||
},
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "14px"
|
||||
fontSize: "14px",
|
||||
},
|
||||
"& .MuiListItem-gutters": {
|
||||
paddingRight: "0px"
|
||||
}
|
||||
}
|
||||
paddingRight: "0px",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mapState = (state: AppState) => ({
|
||||
open: state.system.loggedIn
|
||||
open: state.system.loggedIn,
|
||||
});
|
||||
|
||||
const connector = connect(mapState, { userLoggedIn });
|
||||
@@ -132,6 +133,12 @@ class Menu extends React.Component<MenuProps> {
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Buckets" />
|
||||
</ListItem>
|
||||
<ListItem button component={NavLink} to="/watch">
|
||||
<ListItemIcon>
|
||||
<CenterFocusWeakIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Watch" />
|
||||
</ListItem>
|
||||
<Divider />
|
||||
<ListItem component={Typography}>Admin</ListItem>
|
||||
<ListItem button component={NavLink} to="/users">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -15,13 +15,12 @@
|
||||
// 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 storage from "local-storage-fallback";
|
||||
import { AppState } from "../../../store";
|
||||
import { connect } from "react-redux";
|
||||
import { traceMessageReceived, traceResetMessages } from "./actions";
|
||||
import { TraceMessage } from "./types";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { niceBytes } from "../../../common/utils";
|
||||
import { niceBytes, timeFromDate } from "../../../common/utils";
|
||||
import { wsProtocol } from "../../../utils/wsUtils";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
@@ -92,14 +91,6 @@ const Trace = ({
|
||||
}
|
||||
}, [traceMessageReceived]);
|
||||
|
||||
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()}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Trace</h1>
|
||||
@@ -108,7 +99,7 @@ const Trace = ({
|
||||
{messages.map(m => {
|
||||
return (
|
||||
<li key={m.key}>
|
||||
{timeFromdate(m.time)} - {m.api}[{m.statusCode} {m.statusMsg}]{" "}
|
||||
{timeFromDate(m.time)} - {m.api}[{m.statusCode} {m.statusMsg}]{" "}
|
||||
{m.api} {m.host} {m.client} {m.callStats.duration} ↑{" "}
|
||||
{niceBytes(m.callStats.rx + "")} ↓{" "}
|
||||
{niceBytes(m.callStats.tx + "")}
|
||||
|
||||
@@ -18,6 +18,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 GroupsSelectors from "./GroupsSelectors";
|
||||
import Title from "../../../common/Title";
|
||||
@@ -33,24 +34,25 @@ interface IAddToGroup {
|
||||
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 AddToGroup = ({
|
||||
open,
|
||||
checkedUsers,
|
||||
closeModalAndRefresh,
|
||||
classes
|
||||
classes,
|
||||
}: IAddToGroup) => {
|
||||
//Local States
|
||||
const [saving, isSaving] = useState<boolean>(false);
|
||||
@@ -64,14 +66,14 @@ const AddToGroup = ({
|
||||
api
|
||||
.invoke("PUT", "/api/v1/users-groups-bulk", {
|
||||
groups: selectedGroups,
|
||||
users: checkedUsers
|
||||
users: checkedUsers,
|
||||
})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
isSaving(false);
|
||||
setError("");
|
||||
closeModalAndRefresh(true);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
isSaving(false);
|
||||
setError(err);
|
||||
});
|
||||
@@ -86,7 +88,7 @@ const AddToGroup = ({
|
||||
setError,
|
||||
closeModalAndRefresh,
|
||||
selectedGroups,
|
||||
checkedUsers
|
||||
checkedUsers,
|
||||
]);
|
||||
|
||||
//Fetch Actions
|
||||
@@ -106,35 +108,34 @@ const AddToGroup = ({
|
||||
>
|
||||
<form noValidate autoComplete="off" onSubmit={setSaving}>
|
||||
<Grid container>
|
||||
{updatingError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{updatingError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
{updatingError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{updatingError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Title>Users to be altered</Title>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{checkedUsers.join(", ")}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<GroupsSelectors
|
||||
selectedGroups={selectedGroups}
|
||||
setSelectedGroups={setSelectedGroups}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
<Grid item xs={12}>
|
||||
<Title>Users to be altered</Title>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{checkedUsers.join(", ")}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<GroupsSelectors
|
||||
selectedGroups={selectedGroups}
|
||||
setSelectedGroups={setSelectedGroups}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
|
||||
@@ -19,8 +19,9 @@ 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 api from "../../../common/api";
|
||||
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
|
||||
import { User } from "./types";
|
||||
import api from "../../../common/api";
|
||||
import GroupsSelectors from "./GroupsSelectors";
|
||||
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
@@ -29,17 +30,18 @@ import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/Radi
|
||||
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,
|
||||
});
|
||||
|
||||
interface IAddUserContentProps {
|
||||
@@ -68,7 +70,7 @@ class AddUserContent extends React.Component<
|
||||
accessKey: "",
|
||||
secretKey: "",
|
||||
enabled: "enabled",
|
||||
selectedGroups: []
|
||||
selectedGroups: [],
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -77,7 +79,7 @@ class AddUserContent extends React.Component<
|
||||
this.setState({
|
||||
accessKey: "",
|
||||
secretKey: "",
|
||||
selectedGroups: []
|
||||
selectedGroups: [],
|
||||
});
|
||||
} else {
|
||||
this.getUserInformation();
|
||||
@@ -91,7 +93,7 @@ class AddUserContent extends React.Component<
|
||||
addLoading,
|
||||
secretKey,
|
||||
selectedGroups,
|
||||
enabled
|
||||
enabled,
|
||||
} = this.state;
|
||||
const { selectedUser } = this.props;
|
||||
if (addLoading) {
|
||||
@@ -102,23 +104,23 @@ class AddUserContent extends React.Component<
|
||||
api
|
||||
.invoke("PUT", `/api/v1/users/${selectedUser.accessKey}`, {
|
||||
status: enabled,
|
||||
groups: selectedGroups
|
||||
groups: selectedGroups,
|
||||
})
|
||||
.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 {
|
||||
@@ -126,24 +128,24 @@ class AddUserContent extends React.Component<
|
||||
.invoke("POST", "/api/v1/users", {
|
||||
accessKey,
|
||||
secretKey,
|
||||
groups: selectedGroups
|
||||
groups: selectedGroups,
|
||||
})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
this.setState(
|
||||
{
|
||||
addLoading: false,
|
||||
addError: ""
|
||||
addError: "",
|
||||
},
|
||||
() => {
|
||||
this.props.closeModalAndRefresh();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
addError: err,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -159,19 +161,19 @@ class AddUserContent extends React.Component<
|
||||
|
||||
api
|
||||
.invoke("GET", `/api/v1/users/${selectedUser.accessKey}`)
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: "",
|
||||
accessKey: res.accessKey,
|
||||
selectedGroups: res.memberOf || [],
|
||||
enabled: res.status
|
||||
enabled: res.status,
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
addError: err,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -184,7 +186,7 @@ class AddUserContent extends React.Component<
|
||||
accessKey,
|
||||
secretKey,
|
||||
selectedGroups,
|
||||
enabled
|
||||
enabled,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
@@ -204,68 +206,67 @@ class AddUserContent extends React.Component<
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
|
||||
<InputBoxWrapper
|
||||
id="accesskey-input"
|
||||
name="accesskey-input"
|
||||
label="Access Key"
|
||||
value={accessKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ accessKey: e.target.value });
|
||||
}}
|
||||
disabled={selectedUser !== null}
|
||||
/>
|
||||
|
||||
{selectedUser !== null ? (
|
||||
<RadioGroupSelector
|
||||
currentSelection={enabled}
|
||||
id="user-status"
|
||||
name="user-status"
|
||||
label="Status"
|
||||
onChange={e => {
|
||||
this.setState({ enabled: e.target.value });
|
||||
}}
|
||||
selectorOptions={[
|
||||
{ label: "Enabled", value: "enabled" },
|
||||
{ label: "Disabled", value: "disabled" }
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<InputBoxWrapper
|
||||
id="standard-multiline-static"
|
||||
name="standard-multiline-static"
|
||||
label="Secret Key"
|
||||
type="password"
|
||||
value={secretKey}
|
||||
id="accesskey-input"
|
||||
name="accesskey-input"
|
||||
label="Access Key"
|
||||
value={accessKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ secretKey: e.target.value });
|
||||
this.setState({ accessKey: e.target.value });
|
||||
}}
|
||||
autoComplete="current-password"
|
||||
disabled={selectedUser !== null}
|
||||
/>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<GroupsSelectors
|
||||
selectedGroups={selectedGroups}
|
||||
setSelectedGroups={(elements: string[]) => {
|
||||
this.setState({
|
||||
selectedGroups: elements
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
|
||||
{selectedUser !== null ? (
|
||||
<RadioGroupSelector
|
||||
currentSelection={enabled}
|
||||
id="user-status"
|
||||
name="user-status"
|
||||
label="Status"
|
||||
onChange={(e) => {
|
||||
this.setState({ enabled: e.target.value });
|
||||
}}
|
||||
selectorOptions={[
|
||||
{ label: "Enabled", value: "enabled" },
|
||||
{ label: "Disabled", value: "disabled" },
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<InputBoxWrapper
|
||||
id="standard-multiline-static"
|
||||
name="standard-multiline-static"
|
||||
label="Secret Key"
|
||||
type="password"
|
||||
value={secretKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ secretKey: e.target.value });
|
||||
}}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<GroupsSelectors
|
||||
selectedGroups={selectedGroups}
|
||||
setSelectedGroups={(elements: string[]) => {
|
||||
this.setState({
|
||||
selectedGroups: elements,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
|
||||
259
portal-ui/src/screens/Console/Watch/Watch.tsx
Normal file
259
portal-ui/src/screens/Console/Watch/Watch.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
// 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 {
|
||||
Button,
|
||||
Grid,
|
||||
Typography,
|
||||
TextField
|
||||
} from "@material-ui/core";
|
||||
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
|
||||
import { AppState } from "../../../store";
|
||||
import { connect } from "react-redux";
|
||||
import { watchMessageReceived, watchResetMessages } from "./actions";
|
||||
import { EventInfo, BucketList, Bucket } from "./types";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { niceBytes, timeFromDate } from "../../../common/utils";
|
||||
import { wsProtocol } from "../../../utils/wsUtils";
|
||||
import api from "../../../common/api";
|
||||
import {
|
||||
FormControl,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from "@material-ui/core";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
watchList: {
|
||||
background: "white",
|
||||
maxHeight: "400px",
|
||||
overflow: "auto",
|
||||
"& ul": {
|
||||
margin: "4px",
|
||||
padding: "0px"
|
||||
},
|
||||
"& ul li": {
|
||||
listStyle: "none",
|
||||
margin: "0px",
|
||||
padding: "0px",
|
||||
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: "206px",
|
||||
boxShadow: "0px 3px 6px #00000012"
|
||||
}
|
||||
});
|
||||
|
||||
interface IWatch {
|
||||
classes: any;
|
||||
watchMessageReceived: typeof watchMessageReceived;
|
||||
watchResetMessages: typeof watchResetMessages;
|
||||
messages: EventInfo[];
|
||||
}
|
||||
|
||||
const Watch = ({
|
||||
classes,
|
||||
watchMessageReceived,
|
||||
watchResetMessages,
|
||||
messages
|
||||
}: IWatch) => {
|
||||
const [start, setStart] = useState(false);
|
||||
const [bucketName, setBucketName] = useState("Select Bucket");
|
||||
const [prefix, setPrefix] = useState("");
|
||||
const [suffix, setSuffix] = useState("");
|
||||
const [bucketList, setBucketList] = useState<Bucket[]>([]);
|
||||
|
||||
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();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
watchResetMessages();
|
||||
// begin watch if bucketName in bucketList and start pressed
|
||||
if (start && bucketList.some(bucket => bucket.name === bucketName)) {
|
||||
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/watch/${bucketName}?prefix=${prefix}&suffix=${suffix}`);
|
||||
|
||||
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) => {
|
||||
let m: EventInfo = JSON.parse(message.data.toString());
|
||||
m.Time = new Date(m.Time.toString());
|
||||
m.key = Math.random();
|
||||
watchMessageReceived(m);
|
||||
};
|
||||
c.onclose = () => {
|
||||
clearInterval(interval);
|
||||
console.log("connection closed by server");
|
||||
};
|
||||
return () => {
|
||||
// close websocket on useEffect cleanup
|
||||
c.close(1000);
|
||||
clearInterval(interval);
|
||||
console.log("closing websockets");
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// reset start status
|
||||
setStart(false);
|
||||
}
|
||||
}, [watchMessageReceived, start]);
|
||||
|
||||
const bucketNames = bucketList.map(bucketName => ({
|
||||
label: bucketName.name,
|
||||
value: bucketName.name
|
||||
}));
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Watch</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={start}
|
||||
>
|
||||
<MenuItem
|
||||
value={bucketName}
|
||||
key={`select-bucket-name-default`}
|
||||
disabled={true}
|
||||
>
|
||||
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={start}
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
}}
|
||||
onChange={(e) => { setPrefix(e.target.value) }}
|
||||
/>
|
||||
<TextField
|
||||
placeholder="Suffix"
|
||||
className={classes.inputField}
|
||||
id="suffix-resource"
|
||||
label=""
|
||||
disabled={start}
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
}}
|
||||
onChange={(e) => { setSuffix(e.target.value) }}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={start}
|
||||
onClick={() => setStart(true)}
|
||||
>
|
||||
Start
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<div className={classes.watchList}>
|
||||
<ul>
|
||||
{messages.map(m => {
|
||||
return (
|
||||
<li key={m.key}>
|
||||
{timeFromDate(m.Time)} - {niceBytes(m.Size + "")} - {m.Type} - {m.Path}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const mapState = (state: AppState) => ({
|
||||
messages: state.watch.messages
|
||||
});
|
||||
|
||||
const connector = connect(mapState, {
|
||||
watchMessageReceived: watchMessageReceived,
|
||||
watchResetMessages: watchResetMessages
|
||||
});
|
||||
|
||||
export default connector(withStyles(styles)(Watch));
|
||||
47
portal-ui/src/screens/Console/Watch/actions.ts
Normal file
47
portal-ui/src/screens/Console/Watch/actions.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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 { EventInfo } from "./types";
|
||||
|
||||
export const WATCH_MESSAGE_RECEIVED = "WATCH_MESSAGE_RECEIVED";
|
||||
export const WATCH_RESET_MESSAGES = "WATCH_RESET_MESSAGES";
|
||||
|
||||
interface WatchMessageReceivedAction {
|
||||
type: typeof WATCH_MESSAGE_RECEIVED;
|
||||
message: EventInfo;
|
||||
}
|
||||
|
||||
interface WatchResetMessagesAction {
|
||||
type: typeof WATCH_RESET_MESSAGES;
|
||||
}
|
||||
|
||||
|
||||
export type WatchActionTypes =
|
||||
| WatchMessageReceivedAction
|
||||
| WatchResetMessagesAction;
|
||||
|
||||
export function watchMessageReceived(message: EventInfo) {
|
||||
return {
|
||||
type: WATCH_MESSAGE_RECEIVED,
|
||||
message: message
|
||||
};
|
||||
}
|
||||
|
||||
export function watchResetMessages() {
|
||||
return {
|
||||
type: WATCH_RESET_MESSAGES
|
||||
};
|
||||
}
|
||||
50
portal-ui/src/screens/Console/Watch/reducers.ts
Normal file
50
portal-ui/src/screens/Console/Watch/reducers.ts
Normal 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 {
|
||||
WATCH_MESSAGE_RECEIVED,
|
||||
WATCH_RESET_MESSAGES,
|
||||
WatchActionTypes
|
||||
} from "./actions";
|
||||
import { EventInfo } from "./types";
|
||||
|
||||
export interface WatchState {
|
||||
messages: EventInfo[];
|
||||
}
|
||||
|
||||
const initialState: WatchState = {
|
||||
messages: []
|
||||
};
|
||||
|
||||
export function watchReducer(
|
||||
state = initialState,
|
||||
action: WatchActionTypes
|
||||
): WatchState {
|
||||
switch (action.type) {
|
||||
case WATCH_MESSAGE_RECEIVED:
|
||||
return {
|
||||
...state,
|
||||
messages: [...state.messages, action.message]
|
||||
};
|
||||
case WATCH_RESET_MESSAGES:
|
||||
return {
|
||||
...state,
|
||||
messages: []
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
36
portal-ui/src/screens/Console/Watch/types.ts
Normal file
36
portal-ui/src/screens/Console/Watch/types.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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 EventInfo {
|
||||
Time: Date;
|
||||
Size: number;
|
||||
UserMetadata: Map<string, string>;
|
||||
Path: string;
|
||||
Type: string;
|
||||
Host: string;
|
||||
Port: string;
|
||||
UserAgent: string;
|
||||
key: number;
|
||||
}
|
||||
|
||||
export interface Bucket {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface BucketList {
|
||||
buckets: Bucket[];
|
||||
total: number;
|
||||
}
|
||||
@@ -19,11 +19,13 @@ import thunk from "redux-thunk";
|
||||
import { systemReducer } from "./reducer";
|
||||
import { traceReducer } from "./screens/Console/Trace/reducers";
|
||||
import { logReducer } from "./screens/Console/Logs/reducers";
|
||||
import { watchReducer } from "./screens/Console/Watch/reducers";
|
||||
|
||||
const globalReducer = combineReducers({
|
||||
system: systemReducer,
|
||||
trace: traceReducer,
|
||||
logs: logReducer
|
||||
logs: logReducer,
|
||||
watch: watchReducer,
|
||||
});
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
export const wsProtocol = (protocol: string): string => {
|
||||
let wsProtocol = "ws";
|
||||
if (protocol == "https:") {
|
||||
if (protocol === "https:") {
|
||||
wsProtocol = "wss";
|
||||
}
|
||||
return wsProtocol;
|
||||
|
||||
@@ -39,7 +39,6 @@ func TestAdminConsoleLog(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
mockWSConn := mockConn{}
|
||||
wsClientMock := wsClientMock{madmin: adminClient}
|
||||
function := "startConsoleLog()"
|
||||
|
||||
testReceiver := make(chan madmin.LogInfo, 5)
|
||||
@@ -83,7 +82,7 @@ func TestAdminConsoleLog(t *testing.T) {
|
||||
writesCount++
|
||||
return nil
|
||||
}
|
||||
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); err != nil {
|
||||
if err := startConsoleLog(mockWSConn, adminClient); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// check that the TestReceiver got the same number of data from Console.
|
||||
@@ -95,7 +94,7 @@ func TestAdminConsoleLog(t *testing.T) {
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
return fmt.Errorf("error on write")
|
||||
}
|
||||
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) {
|
||||
if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
|
||||
assert.Equal("error on write", err.Error())
|
||||
}
|
||||
|
||||
@@ -107,7 +106,7 @@ func TestAdminConsoleLog(t *testing.T) {
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""}
|
||||
}
|
||||
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) {
|
||||
if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
|
||||
assert.Equal("websocket: close 1006 (abnormal closure)", err.Error())
|
||||
}
|
||||
|
||||
@@ -116,7 +115,7 @@ func TestAdminConsoleLog(t *testing.T) {
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""}
|
||||
}
|
||||
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); err != nil {
|
||||
if err := startConsoleLog(mockWSConn, adminClient); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
|
||||
@@ -125,7 +124,7 @@ func TestAdminConsoleLog(t *testing.T) {
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""}
|
||||
}
|
||||
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); err != nil {
|
||||
if err := startConsoleLog(mockWSConn, adminClient); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
|
||||
@@ -134,7 +133,7 @@ func TestAdminConsoleLog(t *testing.T) {
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, fmt.Errorf("error on read")
|
||||
}
|
||||
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) {
|
||||
if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
|
||||
assert.Equal("error on read", err.Error())
|
||||
}
|
||||
|
||||
@@ -162,7 +161,7 @@ func TestAdminConsoleLog(t *testing.T) {
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, nil
|
||||
}
|
||||
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) {
|
||||
if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
|
||||
assert.Equal("error on Console", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ func TestAdminTrace(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
mockWSConn := mockConn{}
|
||||
wsClientMock := wsClientMock{madmin: adminClient}
|
||||
function := "startTraceInfo()"
|
||||
|
||||
testReceiver := make(chan shortTraceMsg, 5)
|
||||
@@ -84,7 +83,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
writesCount++
|
||||
return nil
|
||||
}
|
||||
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); err != nil {
|
||||
if err := startTraceInfo(mockWSConn, adminClient); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// check that the TestReceiver got the same number of data from trace.
|
||||
@@ -96,7 +95,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
return fmt.Errorf("error on write")
|
||||
}
|
||||
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) {
|
||||
if err := startTraceInfo(mockWSConn, adminClient); assert.Error(err) {
|
||||
assert.Equal("error on write", err.Error())
|
||||
}
|
||||
|
||||
@@ -108,7 +107,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""}
|
||||
}
|
||||
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) {
|
||||
if err := startTraceInfo(mockWSConn, adminClient); assert.Error(err) {
|
||||
assert.Equal("websocket: close 1006 (abnormal closure)", err.Error())
|
||||
}
|
||||
|
||||
@@ -117,7 +116,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""}
|
||||
}
|
||||
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); err != nil {
|
||||
if err := startTraceInfo(mockWSConn, adminClient); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
|
||||
@@ -126,7 +125,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""}
|
||||
}
|
||||
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); err != nil {
|
||||
if err := startTraceInfo(mockWSConn, adminClient); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
|
||||
@@ -135,7 +134,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, fmt.Errorf("error on read")
|
||||
}
|
||||
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) {
|
||||
if err := startTraceInfo(mockWSConn, adminClient); assert.Error(err) {
|
||||
assert.Equal("error on read", err.Error())
|
||||
}
|
||||
|
||||
@@ -163,7 +162,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, nil
|
||||
}
|
||||
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) {
|
||||
if err := startTraceInfo(mockWSConn, adminClient); assert.Error(err) {
|
||||
assert.Equal("error on trace", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ func NewAdminClient(url, accessKey, secretKey string) (*madmin.AdminClient, *pro
|
||||
AppName: appName,
|
||||
AppVersion: McsVersion,
|
||||
AppComments: []string{appName, runtime.GOOS, runtime.GOARCH},
|
||||
Insecure: false,
|
||||
})
|
||||
s3Client.SetCustomTransport(STSClient.Transport)
|
||||
if err != nil {
|
||||
return nil, err.Trace(url)
|
||||
}
|
||||
@@ -240,13 +242,15 @@ func newMAdminClient(jwt string) (*madmin.AdminClient, error) {
|
||||
|
||||
// newAdminFromClaims creates a minio admin from Decrypted claims using Assume role credentials
|
||||
func newAdminFromClaims(claims *auth.DecryptedClaims) (*madmin.AdminClient, error) {
|
||||
tlsEnabled := getMinIOEndpointIsSecure()
|
||||
adminClient, err := madmin.NewWithOptions(getMinIOEndpoint(), &madmin.Options{
|
||||
Creds: credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken),
|
||||
Secure: getMinIOEndpointIsSecure(),
|
||||
Secure: tlsEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminClient.SetCustomTransport(STSClient.Transport)
|
||||
return adminClient, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,15 @@ package restapi
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"errors"
|
||||
|
||||
mc "github.com/minio/mc/cmd"
|
||||
"github.com/minio/mc/pkg/probe"
|
||||
"github.com/minio/mcs/pkg/auth"
|
||||
xjwt "github.com/minio/mcs/pkg/auth/jwt"
|
||||
"github.com/minio/mcs/pkg/auth/ldap"
|
||||
"github.com/minio/minio-go/v6"
|
||||
"github.com/minio/minio-go/v6/pkg/credentials"
|
||||
)
|
||||
@@ -90,6 +94,7 @@ func (c minioClient) getBucketPolicy(bucketName string) (string, error) {
|
||||
type MCS3Client interface {
|
||||
addNotificationConfig(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error
|
||||
removeNotificationConfig(arn string, event string, prefix string, suffix string) *probe.Error
|
||||
watch(options mc.WatchOptions) (*mc.WatchObject, *probe.Error)
|
||||
}
|
||||
|
||||
// Interface implementation
|
||||
@@ -110,6 +115,10 @@ func (c mcS3Client) removeNotificationConfig(arn string, event string, prefix st
|
||||
return c.client.RemoveNotificationConfig(arn, event, prefix, suffix)
|
||||
}
|
||||
|
||||
func (c mcS3Client) watch(options mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
|
||||
return c.client.Watch(options)
|
||||
}
|
||||
|
||||
// MCSCredentials interface with all functions to be implemented
|
||||
// by mock when testing, it should include all needed minioCredentials.Credentials api calls
|
||||
// that are used within this project.
|
||||
@@ -133,13 +142,62 @@ func (c mcsCredentials) Expire() {
|
||||
c.minioCredentials.Expire()
|
||||
}
|
||||
|
||||
// mcsSTSAssumeRole it's a STSAssumeRole wrapper, in general
|
||||
// there's no need to use this struct anywhere else in the project, it's only required
|
||||
// for passing a custom *http.Client to *credentials.STSAssumeRole
|
||||
type mcsSTSAssumeRole struct {
|
||||
stsAssumeRole *credentials.STSAssumeRole
|
||||
}
|
||||
|
||||
func (s mcsSTSAssumeRole) Retrieve() (credentials.Value, error) {
|
||||
return s.stsAssumeRole.Retrieve()
|
||||
}
|
||||
|
||||
func (s mcsSTSAssumeRole) IsExpired() bool {
|
||||
return s.stsAssumeRole.IsExpired()
|
||||
}
|
||||
|
||||
// STSClient contains http.client configuration need it by STSAssumeRole
|
||||
var STSClient = PrepareSTSClient()
|
||||
|
||||
func newMcsCredentials(accessKey, secretKey, location string) (*credentials.Credentials, error) {
|
||||
return credentials.NewSTSAssumeRole(getMinIOServer(), credentials.STSAssumeRoleOptions{
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
Location: location,
|
||||
DurationSeconds: xjwt.GetMcsSTSAndJWTDurationInSeconds(),
|
||||
})
|
||||
mcsEndpoint := getMinIOServer()
|
||||
if mcsEndpoint == "" {
|
||||
return nil, errors.New("STS endpoint cannot be empty")
|
||||
}
|
||||
if accessKey == "" || secretKey == "" {
|
||||
return nil, errors.New("AssumeRole credentials access/secretkey is mandatory")
|
||||
}
|
||||
|
||||
// Future authentication methods can be added under this switch statement
|
||||
switch {
|
||||
// LDAP authentication for MCS
|
||||
case ldap.GetLDAPEnabled():
|
||||
{
|
||||
creds, err := auth.GetMcsCredentialsFromLDAP(mcsEndpoint, accessKey, secretKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return creds, nil
|
||||
}
|
||||
// default authentication for MCS is via STS (Security Token Service) against MinIO
|
||||
default:
|
||||
{
|
||||
opts := credentials.STSAssumeRoleOptions{
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
Location: location,
|
||||
DurationSeconds: xjwt.GetMcsSTSAndJWTDurationInSeconds(),
|
||||
}
|
||||
stsAssumeRole := &credentials.STSAssumeRole{
|
||||
Client: STSClient,
|
||||
STSEndpoint: mcsEndpoint,
|
||||
Options: opts,
|
||||
}
|
||||
mcsSTSWrapper := mcsSTSAssumeRole{stsAssumeRole: stsAssumeRole}
|
||||
return credentials.New(mcsSTSWrapper), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getMcsCredentialsFromJWT returns the *minioCredentials.Credentials associated to the
|
||||
@@ -160,30 +218,35 @@ func newMinioClient(jwt string) (*minio.Client, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{
|
||||
minioClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{
|
||||
Creds: creds,
|
||||
Secure: getMinIOEndpointIsSecure(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return adminClient, nil
|
||||
minioClient.SetCustomTransport(STSClient.Transport)
|
||||
return minioClient, nil
|
||||
}
|
||||
|
||||
// newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket
|
||||
func newS3BucketClient(bucketName *string) (*mc.S3Client, error) {
|
||||
func newS3BucketClient(jwt string, bucketName string) (*mc.S3Client, error) {
|
||||
endpoint := getMinIOServer()
|
||||
accessKeyID := getAccessKey()
|
||||
secretAccessKey := getSecretKey()
|
||||
useSSL := getMinIOEndpointIsSecure()
|
||||
|
||||
if bucketName != nil {
|
||||
endpoint += fmt.Sprintf("/%s", *bucketName)
|
||||
}
|
||||
s3Config := newS3Config(endpoint, accessKeyID, secretAccessKey, !useSSL)
|
||||
client, err := mc.S3New(s3Config)
|
||||
claims, err := auth.JWTAuthenticate(jwt)
|
||||
if err != nil {
|
||||
return nil, err.Cause
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.TrimSpace(bucketName) != "" {
|
||||
endpoint += fmt.Sprintf("/%s", bucketName)
|
||||
}
|
||||
|
||||
s3Config := newS3Config(endpoint, claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken, !useSSL)
|
||||
client, pErr := mc.S3New(s3Config)
|
||||
if pErr != nil {
|
||||
return nil, pErr.Cause
|
||||
}
|
||||
s3Client, ok := client.(*mc.S3Client)
|
||||
if !ok {
|
||||
@@ -195,7 +258,7 @@ func newS3BucketClient(bucketName *string) (*mc.S3Client, error) {
|
||||
|
||||
// newS3Config simply creates a new Config struct using the passed
|
||||
// parameters.
|
||||
func newS3Config(endpoint, accessKey, secretKey string, isSecure bool) *mc.Config {
|
||||
func newS3Config(endpoint, accessKey, secretKey, sessionToken string, isSecure bool) *mc.Config {
|
||||
// We have a valid alias and hostConfig. We populate the
|
||||
// minioCredentials from the match found in the config file.
|
||||
s3Config := new(mc.Config)
|
||||
@@ -209,6 +272,7 @@ func newS3Config(endpoint, accessKey, secretKey string, isSecure bool) *mc.Confi
|
||||
s3Config.HostURL = endpoint
|
||||
s3Config.AccessKey = accessKey
|
||||
s3Config.SecretKey = secretKey
|
||||
s3Config.SessionToken = sessionToken
|
||||
s3Config.Signature = "S3v4"
|
||||
return s3Config
|
||||
}
|
||||
|
||||
@@ -48,7 +48,17 @@ func getSecretKey() string {
|
||||
}
|
||||
|
||||
func getMinIOServer() string {
|
||||
return env.Get(McsMinIOServer, "http://localhost:9000")
|
||||
return strings.TrimSpace(env.Get(McsMinIOServer, "http://localhost:9000"))
|
||||
}
|
||||
|
||||
// If MCS_MINIO_SERVER_TLS_ROOT_CAS is true mcs will load a list of certificates into the
|
||||
// http.client rootCAs store, this is useful for testing or when working with self-signed certificates
|
||||
func getMinioServerTLSRootCAs() []string {
|
||||
caCertFileNames := strings.TrimSpace(env.Get(McsMinIOServerTLSRootCAs, ""))
|
||||
if caCertFileNames == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(caCertFileNames, ",")
|
||||
}
|
||||
|
||||
func getMinIOEndpoint() string {
|
||||
@@ -67,7 +77,7 @@ func getMinIOEndpointIsSecure() bool {
|
||||
if strings.Contains(server, "://") {
|
||||
parts := strings.Split(server, "://")
|
||||
if len(parts) > 1 {
|
||||
if parts[1] == "https" {
|
||||
if parts[0] == "https" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,16 @@ package restapi
|
||||
|
||||
const (
|
||||
// consts for common configuration
|
||||
McsVersion = `0.1.0`
|
||||
McsAccessKey = "MCS_ACCESS_KEY"
|
||||
McsSecretKey = "MCS_SECRET_KEY"
|
||||
McsMinIOServer = "MCS_MINIO_SERVER"
|
||||
McsProductionMode = "MCS_PRODUCTION_MODE"
|
||||
McsHostname = "MCS_HOSTNAME"
|
||||
McsPort = "MCS_PORT"
|
||||
McsTLSHostname = "MCS_TLS_HOSTNAME"
|
||||
McsTLSPort = "MCS_TLS_PORT"
|
||||
McsVersion = `0.1.0`
|
||||
McsAccessKey = "MCS_ACCESS_KEY"
|
||||
McsSecretKey = "MCS_SECRET_KEY"
|
||||
McsMinIOServer = "MCS_MINIO_SERVER"
|
||||
McsMinIOServerTLSRootCAs = "MCS_MINIO_SERVER_TLS_ROOT_CAS"
|
||||
McsProductionMode = "MCS_PRODUCTION_MODE"
|
||||
McsHostname = "MCS_HOSTNAME"
|
||||
McsPort = "MCS_PORT"
|
||||
McsTLSHostname = "MCS_TLS_HOSTNAME"
|
||||
McsTLSPort = "MCS_TLS_PORT"
|
||||
|
||||
// consts for Secure middleware
|
||||
McsSecureAllowedHosts = "MCS_SECURE_ALLOWED_HOSTS"
|
||||
|
||||
95
restapi/tls.go
Normal file
95
restapi/tls.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// 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 restapi
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
certDontExists = "File certificate doesn't exists: %s"
|
||||
)
|
||||
|
||||
func prepareSTSClientTransport() *http.Transport {
|
||||
// This takes github.com/minio/minio/pkg/madmin/transport.go as an example
|
||||
//
|
||||
// DefaultTransport - this default transport is similar to
|
||||
// http.DefaultTransport but with additional param DisableCompression
|
||||
// is set to true to avoid decompressing content with 'gzip' encoding.
|
||||
DefaultTransport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
KeepAlive: 15 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 1024,
|
||||
MaxIdleConnsPerHost: 1024,
|
||||
ResponseHeaderTimeout: 60 * time.Second,
|
||||
IdleConnTimeout: 60 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
DisableCompression: true,
|
||||
}
|
||||
// If Minio instance is running with TLS enabled and it's using a self-signed certificate
|
||||
// or a certificate issued by a custom certificate authority we prepare a new custom *http.Transport
|
||||
if getMinIOEndpointIsSecure() {
|
||||
caCertFileNames := getMinioServerTLSRootCAs()
|
||||
tlsConfig := &tls.Config{
|
||||
// Can't use SSLv3 because of POODLE and BEAST
|
||||
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
|
||||
// Can't use TLSv1.1 because of RC4 cipher usage
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
// If root CAs are configured we save them to the http.Client RootCAs store
|
||||
if len(caCertFileNames) > 0 {
|
||||
certs := x509.NewCertPool()
|
||||
for _, caCert := range caCertFileNames {
|
||||
// Validate certificate exists
|
||||
if FileExists(caCert) {
|
||||
pemData, err := ioutil.ReadFile(caCert)
|
||||
if err != nil {
|
||||
// if there was an error reading pem file stop mcs
|
||||
panic(err)
|
||||
}
|
||||
certs.AppendCertsFromPEM(pemData)
|
||||
} else {
|
||||
// if provided cert filename doesn't exists stop mcs
|
||||
panic(fmt.Sprintf(certDontExists, caCert))
|
||||
}
|
||||
}
|
||||
tlsConfig.RootCAs = certs
|
||||
}
|
||||
DefaultTransport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
return DefaultTransport
|
||||
}
|
||||
|
||||
// PrepareSTSClient returns an http.Client with custom configurations need it by *credentials.STSAssumeRole
|
||||
// custom configurations include skipVerification flag, and root CA certificates
|
||||
func PrepareSTSClient() *http.Client {
|
||||
transport := prepareSTSClientTransport()
|
||||
// Return http client with default configuration
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
@@ -40,14 +40,16 @@ func registerBucketEventsHandlers(api *operations.McsAPI) {
|
||||
})
|
||||
// create bucket event
|
||||
api.UserAPICreateBucketEventHandler = user_api.CreateBucketEventHandlerFunc(func(params user_api.CreateBucketEventParams, principal *models.Principal) middleware.Responder {
|
||||
if err := getCreateBucketEventsResponse(params.BucketName, params.Body); err != nil {
|
||||
sessionID := string(*principal)
|
||||
if err := getCreateBucketEventsResponse(sessionID, params.BucketName, params.Body); err != nil {
|
||||
return user_api.NewCreateBucketEventDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
|
||||
}
|
||||
return user_api.NewCreateBucketEventCreated()
|
||||
})
|
||||
// delete bucket event
|
||||
api.UserAPIDeleteBucketEventHandler = user_api.DeleteBucketEventHandlerFunc(func(params user_api.DeleteBucketEventParams, principal *models.Principal) middleware.Responder {
|
||||
if err := getDeleteBucketEventsResponse(params.BucketName, params.Arn, params.Body.Events, params.Body.Prefix, params.Body.Suffix); err != nil {
|
||||
sessionID := string(*principal)
|
||||
if err := getDeleteBucketEventsResponse(sessionID, params.BucketName, params.Arn, params.Body.Events, params.Body.Prefix, params.Body.Suffix); err != nil {
|
||||
return user_api.NewDeleteBucketEventDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
|
||||
}
|
||||
return user_api.NewDeleteBucketEventNoContent()
|
||||
@@ -178,8 +180,8 @@ func createBucketEvent(client MCS3Client, arn string, notificationEvents []model
|
||||
}
|
||||
|
||||
// getCreateBucketEventsResponse calls createBucketEvent to add a bucket event notification
|
||||
func getCreateBucketEventsResponse(bucketName string, eventReq *models.BucketEventRequest) error {
|
||||
s3Client, err := newS3BucketClient(swag.String(bucketName))
|
||||
func getCreateBucketEventsResponse(sessionID, bucketName string, eventReq *models.BucketEventRequest) error {
|
||||
s3Client, err := newS3BucketClient(sessionID, bucketName)
|
||||
if err != nil {
|
||||
log.Println("error creating S3Client:", err)
|
||||
return err
|
||||
@@ -214,8 +216,8 @@ func joinNotificationEvents(events []models.NotificationEventType) string {
|
||||
}
|
||||
|
||||
// getDeleteBucketEventsResponse calls deleteBucketEventNotification() to delete a bucket event notification
|
||||
func getDeleteBucketEventsResponse(bucketName string, arn string, events []models.NotificationEventType, prefix, suffix *string) error {
|
||||
s3Client, err := newS3BucketClient(swag.String(bucketName))
|
||||
func getDeleteBucketEventsResponse(sessionID, bucketName string, arn string, events []models.NotificationEventType, prefix, suffix *string) error {
|
||||
s3Client, err := newS3BucketClient(sessionID, bucketName)
|
||||
if err != nil {
|
||||
log.Println("error creating S3Client:", err)
|
||||
return err
|
||||
|
||||
@@ -32,7 +32,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
errorGeneric = errors.New("an error occurred, please try again")
|
||||
errorGeneric = errors.New("an error occurred, please try again")
|
||||
errInvalidCredentials = errors.New("invalid Credentials")
|
||||
)
|
||||
|
||||
func registerLoginHandlers(api *operations.McsAPI) {
|
||||
@@ -48,48 +49,48 @@ func registerLoginHandlers(api *operations.McsAPI) {
|
||||
api.UserAPILoginHandler = user_api.LoginHandlerFunc(func(params user_api.LoginParams) middleware.Responder {
|
||||
loginResponse, err := getLoginResponse(params.Body)
|
||||
if err != nil {
|
||||
return user_api.NewLoginDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
|
||||
return user_api.NewLoginDefault(401).WithPayload(&models.Error{Code: 401, Message: swag.String(err.Error())})
|
||||
}
|
||||
return user_api.NewLoginCreated().WithPayload(loginResponse)
|
||||
})
|
||||
api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
|
||||
loginResponse, err := getLoginOauth2AuthResponse(params.Body)
|
||||
if err != nil {
|
||||
return user_api.NewLoginOauth2AuthDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
|
||||
return user_api.NewLoginOauth2AuthDefault(401).WithPayload(&models.Error{Code: 401, Message: swag.String(err.Error())})
|
||||
}
|
||||
return user_api.NewLoginOauth2AuthCreated().WithPayload(loginResponse)
|
||||
})
|
||||
}
|
||||
|
||||
var errInvalidCredentials = errors.New("invalid minioCredentials")
|
||||
|
||||
// login performs a check of minioCredentials against MinIO
|
||||
func login(credentials MCSCredentials) (*string, error) {
|
||||
// try to obtain minioCredentials,
|
||||
tokens, err := credentials.Get()
|
||||
if err != nil {
|
||||
log.Println("error authenticating user", err)
|
||||
return nil, errInvalidCredentials
|
||||
}
|
||||
// if we made it here, the minioCredentials work, generate a jwt with claims
|
||||
jwt, err := auth.NewJWTWithClaimsForClient(&tokens, getMinIOServer())
|
||||
if err != nil {
|
||||
log.Println("error authenticating user", err)
|
||||
return nil, errInvalidCredentials
|
||||
}
|
||||
return &jwt, nil
|
||||
}
|
||||
|
||||
func getConfiguredRegion(client MinioAdmin) string {
|
||||
func getConfiguredRegionForLogin(client MinioAdmin) (string, error) {
|
||||
location := ""
|
||||
configuration, err := getConfig(client, "region")
|
||||
if err != nil {
|
||||
log.Println("error obtaining MinIO region:", err)
|
||||
return location
|
||||
return location, errorGeneric
|
||||
}
|
||||
// region is an array of 1 element
|
||||
if len(configuration) > 0 {
|
||||
location = configuration[0].Value
|
||||
}
|
||||
return location
|
||||
return location, nil
|
||||
}
|
||||
|
||||
// getLoginResponse performs login() and serializes it to the handler's output
|
||||
@@ -102,16 +103,18 @@ func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, error) {
|
||||
adminClient := adminClient{client: mAdmin}
|
||||
// obtain the configured MinIO region
|
||||
// need it for user authentication
|
||||
location := getConfiguredRegion(adminClient)
|
||||
location, err := getConfiguredRegionForLogin(adminClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds, err := newMcsCredentials(*lr.AccessKey, *lr.SecretKey, location)
|
||||
if err != nil {
|
||||
log.Println("error login:", err)
|
||||
return nil, err
|
||||
return nil, errInvalidCredentials
|
||||
}
|
||||
credentials := mcsCredentials{minioCredentials: creds}
|
||||
sessionID, err := login(credentials)
|
||||
if err != nil {
|
||||
log.Println("error login:", err)
|
||||
return nil, err
|
||||
}
|
||||
// serialize output
|
||||
@@ -131,7 +134,8 @@ func getLoginDetailsResponse() (*models.LoginDetails, error) {
|
||||
// initialize new oauth2 client
|
||||
oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Println("error getting new oauth2 provider client", err)
|
||||
return nil, errorGeneric
|
||||
}
|
||||
// Validate user against IDP
|
||||
identityProvider := &auth.IdentityProvider{Client: oauth2Client}
|
||||
@@ -147,7 +151,8 @@ func getLoginDetailsResponse() (*models.LoginDetails, error) {
|
||||
func loginOauth2Auth(ctx context.Context, provider *auth.IdentityProvider, code, state string) (*oauth2.User, error) {
|
||||
userIdentity, err := provider.VerifyIdentity(ctx, code, state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Println("error validating user identity against idp:", err)
|
||||
return nil, errorGeneric
|
||||
}
|
||||
return userIdentity, nil
|
||||
}
|
||||
@@ -166,8 +171,7 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
|
||||
// Validate user against IDP
|
||||
identity, err := loginOauth2Auth(ctx, identityProvider, *lr.Code, *lr.State)
|
||||
if err != nil {
|
||||
log.Println("error validating user identity against idp:", err)
|
||||
return nil, errorGeneric
|
||||
return nil, err
|
||||
}
|
||||
mAdmin, err := newSuperMAdminClient()
|
||||
if err != nil {
|
||||
@@ -179,7 +183,10 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
|
||||
secretKey := utils.RandomCharString(32)
|
||||
// obtain the configured MinIO region
|
||||
// need it for user authentication
|
||||
location := getConfiguredRegion(adminClient)
|
||||
location, err := getConfiguredRegionForLogin(adminClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// create user in MinIO
|
||||
if _, err := addUser(ctx, adminClient, &accessKey, &secretKey, []string{}); err != nil {
|
||||
log.Println("error adding user:", err)
|
||||
@@ -207,8 +214,7 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
|
||||
credentials := mcsCredentials{minioCredentials: creds}
|
||||
jwt, err := login(credentials)
|
||||
if err != nil {
|
||||
log.Println("error login:", err)
|
||||
return nil, errorGeneric
|
||||
return nil, err
|
||||
}
|
||||
// serialize output
|
||||
loginResponse := &models.LoginResponse{
|
||||
|
||||
@@ -100,7 +100,7 @@ func TestLoginOauth2Auth(t *testing.T) {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
if _, err := loginOauth2Auth(ctx, identityProvider, mockCode, mockState); funcAssert.Error(err) {
|
||||
funcAssert.Equal("error", err.Error())
|
||||
funcAssert.Equal("an error occurred, please try again", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,8 +189,8 @@ func Test_getConfiguredRegion(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
tt.mock()
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getConfiguredRegion(tt.args.client); got != tt.want {
|
||||
t.Errorf("getConfiguredRegion() = %v, want %v", got, tt.want)
|
||||
if got, _ := getConfiguredRegionForLogin(tt.args.client); got != tt.want {
|
||||
t.Errorf("getConfiguredRegionForLogin() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
165
restapi/user_watch.go
Normal file
165
restapi/user_watch.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
mc "github.com/minio/mc/cmd"
|
||||
)
|
||||
|
||||
type watchOptions struct {
|
||||
BucketName string
|
||||
mc.WatchOptions
|
||||
}
|
||||
|
||||
// startWatch starts by setting a websocket reader that
|
||||
// will check for a heartbeat.
|
||||
//
|
||||
// A WaitGroup is used to handle goroutines and to ensure
|
||||
// all finish in the proper order. If any, sendWatchInfo()
|
||||
// or wsReadCheck() returns, watch should end.
|
||||
func startWatch(conn WSConn, client MCS3Client, options watchOptions) (mError error) {
|
||||
// a WaitGroup waits for a collection of goroutines to finish
|
||||
wg := sync.WaitGroup{}
|
||||
// a cancel context is needed to end all goroutines used
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Set number of goroutines to wait. wg.Wait()
|
||||
// waits until counter is zero (all are done)
|
||||
wg.Add(3)
|
||||
// start go routine for reading websocket heartbeat
|
||||
readErr := wsReadCheck(ctx, &wg, conn)
|
||||
// send Stream of watch events to the ws c.connection
|
||||
ch := sendWatchInfo(ctx, &wg, conn, client, options)
|
||||
// If wsReadCheck returns it means that it is not possible to check
|
||||
// ws heartbeat anymore so we stop from doing Watch, cancel context
|
||||
// for all goroutines.
|
||||
go func(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
if err := <-readErr; err != nil {
|
||||
log.Println("error on wsReadCheck:", err)
|
||||
mError = err
|
||||
}
|
||||
// cancel context for all goroutines.
|
||||
cancel()
|
||||
}(&wg)
|
||||
|
||||
if err := <-ch; err != nil {
|
||||
mError = err
|
||||
}
|
||||
|
||||
// if ch closes for any reason,
|
||||
// cancel context for all goroutines
|
||||
cancel()
|
||||
// wait all goroutines to finish
|
||||
wg.Wait()
|
||||
return mError
|
||||
}
|
||||
|
||||
// sendWatchInfo sends stream of Watch Event to the ws connection
|
||||
func sendWatchInfo(ctx context.Context, wg *sync.WaitGroup, conn WSConn, wsc MCS3Client, options watchOptions) <-chan error {
|
||||
// decrements the WaitGroup counter
|
||||
// by one when the function returns
|
||||
defer wg.Done()
|
||||
ch := make(chan error)
|
||||
go func(ch chan<- error) {
|
||||
defer close(ch)
|
||||
wo, pErr := wsc.watch(options.WatchOptions)
|
||||
if pErr != nil {
|
||||
fmt.Println("error initializing watch:", pErr.Cause)
|
||||
ch <- pErr.Cause
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
close(wo.DoneChan)
|
||||
return
|
||||
case events, ok := <-wo.Events():
|
||||
// zero value returned because the channel is closed and empty
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, event := range events {
|
||||
// Serialize message to be sent
|
||||
bytes, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
fmt.Println("error on json.Marshal:", err)
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
// Send Message through websocket connection
|
||||
err = conn.writeMessage(websocket.TextMessage, bytes)
|
||||
if err != nil {
|
||||
log.Println("error writeMessage:", err)
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
case pErr, ok := <-wo.Errors():
|
||||
// zero value returned because the channel is closed and empty
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if pErr != nil {
|
||||
log.Println("error on watch:", pErr.Cause)
|
||||
ch <- pErr.Cause
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
// getOptionsFromReq gets bucket name, events, prefix, suffix from a websocket
|
||||
// watch path if defined.
|
||||
// path come as : `/watch/bucket1` and query params come on request form
|
||||
func getOptionsFromReq(req *http.Request) watchOptions {
|
||||
wOptions := watchOptions{}
|
||||
// Default Events if not defined
|
||||
wOptions.Events = []string{"put", "get", "delete"}
|
||||
|
||||
re := regexp.MustCompile(`(/watch/)(.*?$)`)
|
||||
matches := re.FindAllSubmatch([]byte(req.URL.Path), -1)
|
||||
// len matches is always 3
|
||||
// matches comes as e.g.
|
||||
// [["...", "/watch/" "bucket1"]]
|
||||
// [["/watch/" "/watch/" ""]]
|
||||
|
||||
// bucket name is on the second group, third position
|
||||
wOptions.BucketName = strings.TrimSpace(string(matches[0][2]))
|
||||
|
||||
events := req.FormValue("events")
|
||||
if strings.TrimSpace(events) != "" {
|
||||
wOptions.Events = strings.Split(events, ",")
|
||||
}
|
||||
wOptions.Prefix = req.FormValue("prefix")
|
||||
wOptions.Suffix = req.FormValue("suffix")
|
||||
return wOptions
|
||||
}
|
||||
291
restapi/user_watch_test.go
Normal file
291
restapi/user_watch_test.go
Normal file
@@ -0,0 +1,291 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package restapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
mc "github.com/minio/mc/cmd"
|
||||
"github.com/minio/mc/pkg/probe"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// assigning mock at runtime instead of compile time
|
||||
var mcWatchMock func(options mc.WatchOptions) (*mc.WatchObject, *probe.Error)
|
||||
|
||||
// implements mc.S3Client.Watch()
|
||||
func (c s3ClientMock) watch(options mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
|
||||
return mcWatchMock(options)
|
||||
}
|
||||
|
||||
func TestWatch(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
client := s3ClientMock{}
|
||||
mockWSConn := mockConn{}
|
||||
|
||||
function := "startWatch()"
|
||||
|
||||
testStreamSize := 5
|
||||
testReceiver := make(chan []mc.EventInfo, testStreamSize)
|
||||
textToReceive := "test message"
|
||||
testOptions := watchOptions{}
|
||||
testOptions.BucketName = "bucktest"
|
||||
testOptions.Prefix = "file/"
|
||||
testOptions.Suffix = ".png"
|
||||
|
||||
// Test-1: Serve Watch with no errors until Watch finishes sending
|
||||
// define mock function behavior
|
||||
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
|
||||
wo := &mc.WatchObject{
|
||||
EventInfoChan: make(chan []mc.EventInfo),
|
||||
ErrorChan: make(chan *probe.Error),
|
||||
DoneChan: make(chan struct{}),
|
||||
}
|
||||
// Only success, start a routine to start reading line by line.
|
||||
go func(wo *mc.WatchObject) {
|
||||
defer wo.Close()
|
||||
lines := make([]int, testStreamSize)
|
||||
// mocking sending 5 lines of info
|
||||
for range lines {
|
||||
info := []mc.EventInfo{
|
||||
mc.EventInfo{
|
||||
UserAgent: textToReceive,
|
||||
},
|
||||
}
|
||||
wo.Events() <- info
|
||||
}
|
||||
}(wo)
|
||||
return wo, nil
|
||||
}
|
||||
// mock function of conn.ReadMessage(), no error on read
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, nil
|
||||
}
|
||||
writesCount := 1
|
||||
// mock connection WriteMessage() no error
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
// emulate that receiver gets the message written
|
||||
var t []mc.EventInfo
|
||||
_ = json.Unmarshal(data, &t)
|
||||
if writesCount == testStreamSize {
|
||||
// for testing we need to close the receiver channel
|
||||
close(testReceiver)
|
||||
return nil
|
||||
}
|
||||
testReceiver <- t
|
||||
writesCount++
|
||||
return nil
|
||||
}
|
||||
if err := startWatch(mockWSConn, client, testOptions); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// check that the TestReceiver got the same number of data from Console.
|
||||
for i := range testReceiver {
|
||||
for _, val := range i {
|
||||
assert.Equal(textToReceive, val.UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
// Test-2: if error happens while writing, return error
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
return fmt.Errorf("error on write")
|
||||
}
|
||||
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
|
||||
assert.Equal("error on write", err.Error())
|
||||
}
|
||||
|
||||
// Test-3: error happens while reading, unexpected Close Error should return error.
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
return nil
|
||||
}
|
||||
// mock function of conn.ReadMessage(), returns unexpected Close Error CloseAbnormalClosure
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""}
|
||||
}
|
||||
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
|
||||
assert.Equal("websocket: close 1006 (abnormal closure)", err.Error())
|
||||
}
|
||||
|
||||
// Test-4: error happens while reading, expected Close Error NormalClosure
|
||||
// expected Close Error should not return an error, just end Console.
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""}
|
||||
}
|
||||
if err := startWatch(mockWSConn, client, testOptions); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
|
||||
// Test-5: error happens while reading, expected Close Error CloseGoingAway
|
||||
// expected Close Error should not return an error, just return.
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""}
|
||||
}
|
||||
if err := startWatch(mockWSConn, client, testOptions); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
|
||||
// Test-6: error happens while reading, non Close Error Type should be returned as
|
||||
// error
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, fmt.Errorf("error on read")
|
||||
}
|
||||
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
|
||||
assert.Equal("error on read", err.Error())
|
||||
}
|
||||
|
||||
// Test-7: error happens on Watch, watch should stop
|
||||
// and error shall be returned.
|
||||
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
|
||||
wo := &mc.WatchObject{
|
||||
EventInfoChan: make(chan []mc.EventInfo),
|
||||
ErrorChan: make(chan *probe.Error),
|
||||
DoneChan: make(chan struct{}),
|
||||
}
|
||||
// Only success, start a routine to start reading line by line.
|
||||
go func(wo *mc.WatchObject) {
|
||||
defer wo.Close()
|
||||
lines := make([]int, testStreamSize)
|
||||
// mocking sending 5 lines of info
|
||||
for range lines {
|
||||
info := []mc.EventInfo{
|
||||
mc.EventInfo{
|
||||
UserAgent: textToReceive,
|
||||
},
|
||||
}
|
||||
wo.Events() <- info
|
||||
}
|
||||
wo.Errors() <- &probe.Error{Cause: fmt.Errorf("error on Watch")}
|
||||
}(wo)
|
||||
return wo, nil
|
||||
}
|
||||
// mock function of conn.ReadMessage(), no error on read, should stay unless
|
||||
// context is done.
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, nil
|
||||
}
|
||||
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
|
||||
assert.Equal("error on Watch", err.Error())
|
||||
}
|
||||
|
||||
// Test-8: error happens on Watch, watch should stop
|
||||
// and error shall be returned.
|
||||
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
|
||||
return nil, &probe.Error{Cause: fmt.Errorf("error on Watch")}
|
||||
}
|
||||
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
|
||||
assert.Equal("error on Watch", err.Error())
|
||||
}
|
||||
|
||||
// Test-9: return nil on error on Watch
|
||||
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
|
||||
wo := &mc.WatchObject{
|
||||
EventInfoChan: make(chan []mc.EventInfo),
|
||||
ErrorChan: make(chan *probe.Error),
|
||||
DoneChan: make(chan struct{}),
|
||||
}
|
||||
// Only success, start a routine to start reading line by line.
|
||||
go func(wo *mc.WatchObject) {
|
||||
defer wo.Close()
|
||||
lines := make([]int, testStreamSize)
|
||||
// mocking sending 5 lines of info
|
||||
for range lines {
|
||||
info := []mc.EventInfo{
|
||||
mc.EventInfo{
|
||||
UserAgent: textToReceive,
|
||||
},
|
||||
}
|
||||
wo.Events() <- info
|
||||
}
|
||||
wo.Events() <- nil
|
||||
wo.Errors() <- nil
|
||||
}(wo)
|
||||
return wo, nil
|
||||
}
|
||||
if err := startWatch(mockWSConn, client, testOptions); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// check that the TestReceiver got the same number of data from Console.
|
||||
for i := range testReceiver {
|
||||
for _, val := range i {
|
||||
assert.Equal(textToReceive, val.UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
// Test-9: getOptionsFromReq return parameters from path
|
||||
u, err := url.Parse("http://localhost/api/v1/watch/bucket1?prefix=&suffix=.jpg&events=put,get")
|
||||
if err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
|
||||
}
|
||||
req := &http.Request{
|
||||
URL: u,
|
||||
}
|
||||
opts := getOptionsFromReq(req)
|
||||
expectedOptions := watchOptions{
|
||||
BucketName: "bucket1",
|
||||
}
|
||||
expectedOptions.Prefix = ""
|
||||
expectedOptions.Suffix = ".jpg"
|
||||
expectedOptions.Events = []string{"put", "get"}
|
||||
assert.Equal(expectedOptions.BucketName, opts.BucketName)
|
||||
assert.Equal(expectedOptions.Prefix, opts.Prefix)
|
||||
assert.Equal(expectedOptions.Suffix, opts.Suffix)
|
||||
assert.Equal(expectedOptions.Events, opts.Events)
|
||||
|
||||
// Test-9: getOptionsFromReq return default events if not defined
|
||||
u, err = url.Parse("http://localhost/api/v1/watch/bucket1?prefix=&suffix=.jpg&events=")
|
||||
if err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
|
||||
}
|
||||
req = &http.Request{
|
||||
URL: u,
|
||||
}
|
||||
opts = getOptionsFromReq(req)
|
||||
expectedOptions = watchOptions{
|
||||
BucketName: "bucket1",
|
||||
}
|
||||
expectedOptions.Prefix = ""
|
||||
expectedOptions.Suffix = ".jpg"
|
||||
expectedOptions.Events = []string{"put", "get", "delete"}
|
||||
assert.Equal(expectedOptions.BucketName, opts.BucketName)
|
||||
assert.Equal(expectedOptions.Prefix, opts.Prefix)
|
||||
assert.Equal(expectedOptions.Suffix, opts.Suffix)
|
||||
assert.Equal(expectedOptions.Events, opts.Events)
|
||||
|
||||
// Test-10: getOptionsFromReq return default events if not defined
|
||||
u, err = url.Parse("http://localhost/api/v1/watch/bucket2?prefix=&suffix=")
|
||||
if err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
|
||||
}
|
||||
req = &http.Request{
|
||||
URL: u,
|
||||
}
|
||||
opts = getOptionsFromReq(req)
|
||||
expectedOptions = watchOptions{
|
||||
BucketName: "bucket2",
|
||||
}
|
||||
expectedOptions.Events = []string{"put", "get", "delete"}
|
||||
assert.Equal(expectedOptions.BucketName, opts.BucketName)
|
||||
assert.Equal(expectedOptions.Prefix, opts.Prefix)
|
||||
assert.Equal(expectedOptions.Suffix, opts.Suffix)
|
||||
assert.Equal(expectedOptions.Events, opts.Events)
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package restapi
|
||||
|
||||
import "os"
|
||||
|
||||
// DifferenceArrays returns the elements in `a` that aren't in `b`.
|
||||
func DifferenceArrays(a, b []string) []string {
|
||||
mb := make(map[string]struct{}, len(b))
|
||||
@@ -54,3 +56,12 @@ func UniqueKeys(a []string) []string {
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// FileExists verifies if a file exist on the desired location and its not a folder
|
||||
func FileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/minio/mcs/pkg/auth"
|
||||
"github.com/minio/mcs/pkg/ws"
|
||||
)
|
||||
|
||||
@@ -46,17 +47,29 @@ const (
|
||||
maxMessageSize = 512
|
||||
)
|
||||
|
||||
// MCSWebsocket interface of a Websocket Client
|
||||
type MCSWebsocket interface {
|
||||
// start trace info from servers
|
||||
// MCSWebsocketAdmin interface of a Websocket Client
|
||||
type MCSWebsocketAdmin interface {
|
||||
trace()
|
||||
console()
|
||||
}
|
||||
|
||||
type wsClient struct {
|
||||
type wsAdminClient struct {
|
||||
// websocket connection.
|
||||
conn wsConn
|
||||
// MinIO admin Client
|
||||
madmin MinioAdmin
|
||||
client MinioAdmin
|
||||
}
|
||||
|
||||
// MCSWebsocket interface of a Websocket Client
|
||||
type MCSWebsocket interface {
|
||||
watch(options watchOptions)
|
||||
}
|
||||
|
||||
type wsS3Client struct {
|
||||
// websocket connection.
|
||||
conn wsConn
|
||||
// mcS3Client
|
||||
client MCS3Client
|
||||
}
|
||||
|
||||
// WSConn interface with all functions to be implemented
|
||||
@@ -106,14 +119,18 @@ func (c wsConn) readMessage() (messageType int, p []byte, err error) {
|
||||
// Websocket communication will be done depending
|
||||
// on the path.
|
||||
// Request should come like ws://<host>:<port>/ws/<api>
|
||||
//
|
||||
// TODO: Enable CORS
|
||||
func serveWS(w http.ResponseWriter, req *http.Request) {
|
||||
sessionID, err := ws.GetTokenFromRequest(req)
|
||||
if err != nil {
|
||||
errors.ServeError(w, req, err)
|
||||
return
|
||||
}
|
||||
// Perform authentication before upgrading to a Websocket Connection
|
||||
// authenticate WS connection with MCS
|
||||
claims, err := ws.Authenticate(req)
|
||||
claims, err := auth.JWTAuthenticate(*sessionID)
|
||||
if err != nil {
|
||||
log.Print("error on ws authentication: ", err)
|
||||
errors.ServeError(w, req, err)
|
||||
errors.ServeError(w, req, errors.New(http.StatusUnauthorized, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -125,28 +142,30 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Only start Websocket Interaction after user has been
|
||||
// authenticated with MinIO
|
||||
mAdmin, err := newAdminFromClaims(claims)
|
||||
if err != nil {
|
||||
log.Println("error creating Madmin Client:", err)
|
||||
errors.ServeError(w, req, err)
|
||||
return
|
||||
}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := adminClient{client: mAdmin}
|
||||
// create a websocket connection interface implementation
|
||||
// defining the connection to be used
|
||||
wsConnection := wsConn{conn: conn}
|
||||
|
||||
// create websocket client and handle request
|
||||
wsClient := &wsClient{conn: wsConnection, madmin: adminClient}
|
||||
switch strings.TrimPrefix(req.URL.Path, wsBasePath) {
|
||||
case "/trace":
|
||||
go wsClient.trace()
|
||||
case "/console":
|
||||
go wsClient.console()
|
||||
wsPath := strings.TrimPrefix(req.URL.Path, wsBasePath)
|
||||
switch {
|
||||
case wsPath == "/trace":
|
||||
wsAdminClient, err := newWebSocketAdminClient(conn, claims)
|
||||
if err != nil {
|
||||
errors.ServeError(w, req, err)
|
||||
return
|
||||
}
|
||||
go wsAdminClient.trace()
|
||||
case wsPath == "/console":
|
||||
wsAdminClient, err := newWebSocketAdminClient(conn, claims)
|
||||
if err != nil {
|
||||
errors.ServeError(w, req, err)
|
||||
return
|
||||
}
|
||||
go wsAdminClient.console()
|
||||
case strings.HasPrefix(wsPath, `/watch`):
|
||||
wOptions := getOptionsFromReq(req)
|
||||
wsS3Client, err := newWebSocketS3Client(conn, *sessionID, wOptions.BucketName)
|
||||
if err != nil {
|
||||
errors.ServeError(w, req, err)
|
||||
return
|
||||
}
|
||||
go wsS3Client.watch(wOptions)
|
||||
default:
|
||||
// path not found
|
||||
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
@@ -154,6 +173,51 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// newWebSocketAdminClient returns a wsAdminClient authenticated as an admin user
|
||||
func newWebSocketAdminClient(conn *websocket.Conn, autClaims *auth.DecryptedClaims) (*wsAdminClient, error) {
|
||||
// Only start Websocket Interaction after user has been
|
||||
// authenticated with MinIO
|
||||
mAdmin, err := newAdminFromClaims(autClaims)
|
||||
if err != nil {
|
||||
log.Println("error creating Madmin Client:", err)
|
||||
// close connection
|
||||
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
// create a websocket connection interface implementation
|
||||
// defining the connection to be used
|
||||
wsConnection := wsConn{conn: conn}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := adminClient{client: mAdmin}
|
||||
// create websocket client and handle request
|
||||
wsAdminClient := &wsAdminClient{conn: wsConnection, client: adminClient}
|
||||
return wsAdminClient, nil
|
||||
}
|
||||
|
||||
// newWebSocketS3Client returns a wsAdminClient authenticated as MCS admin
|
||||
func newWebSocketS3Client(conn *websocket.Conn, jwt, bucketName string) (*wsS3Client, error) {
|
||||
// Only start Websocket Interaction after user has been
|
||||
// authenticated with MinIO
|
||||
s3Client, err := newS3BucketClient(jwt, bucketName)
|
||||
if err != nil {
|
||||
log.Println("error creating S3Client:", err)
|
||||
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
// create a websocket connection interface implementation
|
||||
// defining the connection to be used
|
||||
wsConnection := wsConn{conn: conn}
|
||||
// create a s3Client interface implementation
|
||||
// defining the client to be used
|
||||
mcS3C := mcS3Client{client: s3Client}
|
||||
// create websocket client and handle request
|
||||
wsS3Client := &wsS3Client{conn: wsConnection, client: mcS3C}
|
||||
return wsS3Client, nil
|
||||
}
|
||||
|
||||
// wsReadCheck ensures that the client is sending a message
|
||||
// every `pingWait` seconds. If deadline exceeded or an error
|
||||
// happened this will return, meaning it won't be able to ensure
|
||||
@@ -206,7 +270,7 @@ func wsReadCheck(ctx context.Context, wg *sync.WaitGroup, conn WSConn) chan erro
|
||||
|
||||
// trace serves madmin.ServiceTraceInfo
|
||||
// on a Websocket connection.
|
||||
func (wsc *wsClient) trace() {
|
||||
func (wsc *wsAdminClient) trace() {
|
||||
defer func() {
|
||||
log.Println("trace stopped")
|
||||
// close connection after return
|
||||
@@ -214,7 +278,7 @@ func (wsc *wsClient) trace() {
|
||||
}()
|
||||
log.Println("trace started")
|
||||
|
||||
err := startTraceInfo(wsc.conn, wsc.madmin)
|
||||
err := startTraceInfo(wsc.conn, wsc.client)
|
||||
// Send Connection Close Message indicating the Status Code
|
||||
// see https://tools.ietf.org/html/rfc6455#page-45
|
||||
if err != nil {
|
||||
@@ -237,7 +301,7 @@ func (wsc *wsClient) trace() {
|
||||
|
||||
// console serves madmin.GetLogs
|
||||
// on a Websocket connection.
|
||||
func (wsc *wsClient) console() {
|
||||
func (wsc *wsAdminClient) console() {
|
||||
defer func() {
|
||||
log.Println("console logs stopped")
|
||||
// close connection after return
|
||||
@@ -245,7 +309,36 @@ func (wsc *wsClient) console() {
|
||||
}()
|
||||
log.Println("console logs started")
|
||||
|
||||
err := startConsoleLog(wsc.conn, wsc.madmin)
|
||||
err := startConsoleLog(wsc.conn, wsc.client)
|
||||
// Send Connection Close Message indicating the Status Code
|
||||
// see https://tools.ietf.org/html/rfc6455#page-45
|
||||
if err != nil {
|
||||
// If connection exceeded read deadline send Close
|
||||
// Message Policy Violation code since we don't want
|
||||
// to let the receiver figure out the read deadline.
|
||||
// This is a generic code designed if there is a
|
||||
// need to hide specific details about the policy.
|
||||
if nErr, ok := err.(net.Error); ok && nErr.Timeout() {
|
||||
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.ClosePolicyViolation, ""))
|
||||
return
|
||||
}
|
||||
// else, internal server error
|
||||
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, err.Error()))
|
||||
return
|
||||
}
|
||||
// normal closure
|
||||
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
}
|
||||
|
||||
func (wsc *wsS3Client) watch(params watchOptions) {
|
||||
defer func() {
|
||||
log.Println("watch stopped")
|
||||
// close connection after return
|
||||
wsc.conn.close()
|
||||
}()
|
||||
log.Println("watch started")
|
||||
|
||||
err := startWatch(wsc.conn, wsc.client, params)
|
||||
// Send Connection Close Message indicating the Status Code
|
||||
// see https://tools.ietf.org/html/rfc6455#page-45
|
||||
if err != nil {
|
||||
|
||||
@@ -51,18 +51,3 @@ func (c mockConn) setReadDeadline(t time.Time) error {
|
||||
}
|
||||
func (c mockConn) setPongHandler(h func(appData string) error) {
|
||||
}
|
||||
|
||||
// Common mocks for MCSWebsocket interface
|
||||
// assigning mock at runtime instead of compile time
|
||||
var wsTraceMock func()
|
||||
|
||||
// Define a mock struct of wsClient interface implementation
|
||||
type wsClientMock struct {
|
||||
// MinIO admin Client
|
||||
madmin MinioAdmin
|
||||
}
|
||||
|
||||
// mock function of wsc.trace()
|
||||
func (wsc wsClientMock) trace() {
|
||||
wsTraceMock()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user