Compare commits

...

10 Commits

Author SHA1 Message Date
Daniel Valdivia
35c3b53a23 Dashboard Loading + Prettier files (#124) 2020-05-15 14:41:27 -07:00
César Nieto
6fef30f29d Add Watch api and UI integration (#120)
Uses a similar approach as Trace and Console Logs by using
websockets. It also includes the integration with the UI which
needs 3 input fields that are sent as query parameters.
2020-05-15 14:24:29 -07:00
Alex
acf480fd25 Changed help icon position next to label (#119) 2020-05-12 19:54:24 -05:00
César Nieto
8bbc4f0192 remove color dependency and any non ascii characters (#118) 2020-05-12 17:20:58 -07:00
Alex
48e7991f11 Fixed label size & added text alignment (#112)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-12 14:34:48 -07:00
Alex
5e9b0652b0 Changed modal forms to contain scrollbars (#113)
Changed modal forms to contain scrollbars where the inputs are, so this way the titles and buttons get always visible.

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

How to test:

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

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

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

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

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

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

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

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

Change the password for user billy

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

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

Add the mcsAdmin policy to user billy on MinIO

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

Run MinIO

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

Run MCS

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

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

Note: TLS Skip Verification is not supported unless there's a clear need
for it
2020-05-08 17:11:47 -07:00
56 changed files with 2426 additions and 1046 deletions

127
DEVELOPMENT.md Normal file
View File

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

View File

@@ -68,6 +68,15 @@ export MCS_MINIO_SERVER=http://localhost:9000
./mcs server ./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/...` You can verify that the apis work by doing the request on `localhost:9090/api/v1/...`
# Contribute to mcs Project # Contribute to mcs Project

4
go.mod
View File

@@ -17,9 +17,9 @@ require (
github.com/jessevdk/go-flags v1.4.0 github.com/jessevdk/go-flags v1.4.0
github.com/json-iterator/go v1.1.9 github.com/json-iterator/go v1.1.9
github.com/minio/cli v1.22.0 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 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/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1

12
go.sum
View File

@@ -12,6 +12,7 @@ github.com/Azure/azure-storage-blob-go v0.8.0 h1:53qhf0Oxa0nOjgbDeeYPUeyiNmafAFE
github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= github.com/Azure/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 h1:M2YZIajBBVekV86x0rr1443Lc1F/Ylxb9w+5EtSyX3Q=
github.com/Azure/go-autorest v11.7.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 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/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/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= 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/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/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/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/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-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM= 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 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= 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/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 h1:M+TYzBcNIRyzPRg66ndEqUMd7oWDmhvdQmaPC6EZNwM=
github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2/go.mod h1:RDu/qcrnpEdJC/p8tx34+YBFqqX71lB7dOX9QE+ZC4M= 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= 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-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/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-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-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-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= 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/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 h1:AVvILxA976xc27hstd1oR+X9PQG0sPSom1MNb1ImfUs=
github.com/minio/lsync v1.0.1/go.mod h1:tCFzfo0dlvdGl70IT4IAK/5Wtgb0/BrTmo/jE8pArKA= 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-20200515191050-09c31c4ab28c h1:G4ZTNwiiJ73folxqNXZpWQofxus2fGJG7hKxYNrtvRM=
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c/go.mod h1:l9PuOY62zT7AQJqopDjfo/T22AIBJSb2yXPVZf4RlhM= github.com/minio/mc v0.0.0-20200515191050-09c31c4ab28c/go.mod h1:U3Jgk0bcSjn+QPUMisrS6nxCWOoQ6rYWSvLCB30apuU=
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab/go.mod h1:v8oQPMMaTkjDwp5cOz1WCElA4Ik+X+0y4On+VMk0fis= 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 h1:QxIz36O01LbKqJiz6HKeKCOC2afgydspkpahQ807msY=
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0/go.mod h1:Vhlqz7Se0EgpgFiVxpvzF4Zz/h2LMx+EPKH96Aera5U= 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 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.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 h1:nZEve4vdUhwHBoV18zRvPDgjL6NYyDJE5QJvz3l9bRs=
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI= github.com/minio/minio-go/v6 v6.0.55-0.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 h1:pUSI/WKPdd77gcuoJkSzhJ4wdS8OMDOsOu99MtpXEQA=
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61/go.mod h1:4trzEJ7N1nBTd5Tt7OCZT5SEin+WiAXpdJ/WgPkESA8= 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= github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=

39
pkg/auth/ldap.go Normal file
View File

@@ -0,0 +1,39 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package auth
import (
"errors"
"log"
"github.com/minio/minio-go/v6/pkg/credentials"
)
var (
errInvalidCredentials = errors.New("invalid Credentials")
)
// GetMcsCredentialsFromLDAP authenticates the user against MinIO when the LDAP integration is enabled
// if the authentication succeed *credentials.Credentials object is returned and we continue with the normal STSAssumeRole flow
func GetMcsCredentialsFromLDAP(endpoint, ldapUser, ldapPassword string) (*credentials.Credentials, error) {
creds, err := credentials.NewLDAPIdentity(endpoint, ldapUser, ldapPassword)
if err != nil {
log.Println("LDAP authentication error: ", err)
return nil, errInvalidCredentials
}
return creds, nil
}

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

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

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

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

View File

@@ -22,13 +22,14 @@ import (
"strings" "strings"
"github.com/go-openapi/errors" "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>" // 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 // Get Auth token
var reqToken string var reqToken string
@@ -46,11 +47,5 @@ func Authenticate(r *http.Request) (*auth.DecryptedClaims, error) {
} else { } else {
reqToken = strings.TrimSpace(tokenCookie.Value) reqToken = strings.TrimSpace(tokenCookie.Value)
} }
return swag.String(reqToken), nil
// 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
} }

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -38,3 +38,12 @@ export const setCookie = (name: string, val: string) => {
document.cookie = document.cookie =
name + "=" + value + "; expires=" + date.toUTCString() + "; path=/"; 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()}`;
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,50 +32,50 @@ import TableWrapper from "../../Common/TableWrapper/TableWrapper";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
seeMore: { seeMore: {
marginTop: theme.spacing(3) marginTop: theme.spacing(3),
}, },
paper: { paper: {
display: "flex", display: "flex",
overflow: "auto", overflow: "auto",
flexDirection: "column" flexDirection: "column",
}, },
addSideBar: { addSideBar: {
width: "320px", width: "320px",
padding: "20px" padding: "20px",
}, },
errorBlock: { errorBlock: {
color: "red" color: "red",
}, },
tableToolbar: { tableToolbar: {
paddingLeft: theme.spacing(2), paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0) paddingRight: theme.spacing(0),
}, },
minTableHeader: { minTableHeader: {
color: "#393939", color: "#393939",
"& tr": { "& tr": {
"& th": { "& th": {
fontWeight: "bold" fontWeight: "bold",
} },
} },
}, },
actionsTray: { actionsTray: {
textAlign: "right", textAlign: "right",
"& button": { "& button": {
marginLeft: 10 marginLeft: 10,
} },
}, },
searchField: { searchField: {
background: "#FFFFFF", background: "#FFFFFF",
padding: 12, padding: 12,
borderRadius: 5, borderRadius: 5,
boxShadow: "0px 3px 6px #00000012" boxShadow: "0px 3px 6px #00000012",
}, },
noRecords: { noRecords: {
lineHeight: "24px", lineHeight: "24px",
textAlign: "center", textAlign: "center",
padding: "20px" padding: "20px",
} },
}); });
interface IViewBucketProps { interface IViewBucketProps {
@@ -113,7 +113,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
addScreenOpen: false, addScreenOpen: false,
deleteOpen: false, deleteOpen: false,
selectedBucket: "", selectedBucket: "",
selectedEvent: null selectedEvent: null,
}; };
fetchEvents() { fetchEvents() {
@@ -131,7 +131,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
loading: false, loading: false,
records: events || [], records: events || [],
totalRecords: total, totalRecords: total,
error: "" error: "",
}); });
// if we get 0 results, and page > 0 , go down 1 page // if we get 0 results, and page > 0 , go down 1 page
if ((!events || res.events.length === 0) && page > 0) { if ((!events || res.events.length === 0) && page > 0) {
@@ -169,7 +169,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
.then((res: BucketInfo) => { .then((res: BucketInfo) => {
this.setState({ info: res }); this.setState({ info: res });
}) })
.catch(err => {}); .catch((err) => {});
} }
componentDidMount(): void { componentDidMount(): void {
@@ -191,7 +191,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
rowsPerPage, rowsPerPage,
deleteOpen, deleteOpen,
addScreenOpen, addScreenOpen,
selectedEvent selectedEvent,
} = this.state; } = this.state;
const offset = page * rowsPerPage; const offset = page * rowsPerPage;
@@ -228,21 +228,26 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
return ( return (
<React.Fragment> <React.Fragment>
<AddEvent {addScreenOpen && (
open={addScreenOpen} <AddEvent
selectedBucket={bucketName} open={addScreenOpen}
closeModalAndRefresh={() => { selectedBucket={bucketName}
this.setState({ addScreenOpen: false }); closeModalAndRefresh={() => {
this.fetchEvents(); this.setState({ addScreenOpen: false });
}} this.fetchEvents();
/> }}
<SetAccessPolicy />
bucketName={bucketName} )}
open={setAccessPolicyScreenOpen} {setAccessPolicyScreenOpen && (
closeModalAndRefresh={() => { <SetAccessPolicy
this.closeAddModalAndRefresh(); bucketName={bucketName}
}} open={setAccessPolicyScreenOpen}
/> closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
/>
)}
<Grid container> <Grid container>
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="h6"> <Typography variant="h6">
@@ -261,7 +266,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
color="primary" color="primary"
onClick={() => { onClick={() => {
this.setState({ this.setState({
setAccessPolicyScreenOpen: true setAccessPolicyScreenOpen: true,
}); });
}} }}
> >
@@ -285,7 +290,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
startIcon={<CreateIcon />} startIcon={<CreateIcon />}
onClick={() => { onClick={() => {
this.setState({ this.setState({
addScreenOpen: true addScreenOpen: true,
}); });
}} }}
> >
@@ -303,10 +308,10 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
{ {
label: "Events", label: "Events",
elementKey: "events", elementKey: "events",
renderFunction: eventsDisplay renderFunction: eventsDisplay,
}, },
{ label: "Prefix", elementKey: "prefix" }, { label: "Prefix", elementKey: "prefix" },
{ label: "Suffix", elementKey: "suffix" } { label: "Suffix", elementKey: "suffix" },
]} ]}
isLoading={loading} isLoading={loading}
records={filteredRecords} records={filteredRecords}
@@ -320,11 +325,11 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
page: page, page: page,
SelectProps: { SelectProps: {
inputProps: { "aria-label": "rows per page" }, inputProps: { "aria-label": "rows per page" },
native: true native: true,
}, },
onChangePage: handleChangePage, onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage, onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions ActionsComponent: MinTablePaginationActions,
}} }}
/> />
</Grid> </Grid>

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,17 +19,45 @@
export const fieldBasic = { export const fieldBasic = {
inputLabel: { inputLabel: {
fontWeight: 500, fontWeight: 500,
marginRight: 16, marginRight: 10,
minWidth: 90, width: 100,
fontSize: 14, 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: { fieldContainer: {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
marginBottom: 10 marginBottom: 10,
}, },
tooltipContainer: { 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,
},
}; };

View File

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

View File

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

View File

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

View File

@@ -16,35 +16,37 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import get from "lodash/get"; import get from "lodash/get";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, LinearProgress } from "@material-ui/core"; import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper"; import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import api from "../../../../common/api"; import api from "../../../../common/api";
import { serverNeedsRestart } from "../../../../actions";
import { connect } from "react-redux";
import ConfTargetGeneric from "../ConfTargetGeneric"; import ConfTargetGeneric from "../ConfTargetGeneric";
import { serverNeedsRestart } from "../../../../actions";
import { fieldBasic } from "../../Common/FormComponents/common/styleLibrary";
import { fieldsConfigurations, removeEmptyFields } from "../utils"; import { fieldsConfigurations, removeEmptyFields } from "../utils";
import { IConfigurationElement, IElementValue } from "../types"; import { IConfigurationElement, IElementValue } from "../types";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
...fieldBasic,
errorBlock: { errorBlock: {
color: "red" color: "red",
}, },
strongText: { strongText: {
fontWeight: 700 fontWeight: 700,
}, },
keyName: { keyName: {
marginLeft: 5 marginLeft: 5,
}, },
buttonContainer: { buttonContainer: {
textAlign: "right" textAlign: "right",
}, },
logoButton: { logoButton: {
height: "80px" height: "80px",
} },
}); });
interface IAddNotificationEndpointProps { interface IAddNotificationEndpointProps {
@@ -60,7 +62,7 @@ const EditConfiguration = ({
closeModalAndRefresh, closeModalAndRefresh,
serverNeedsRestart, serverNeedsRestart,
selectedConfiguration, selectedConfiguration,
classes classes,
}: IAddNotificationEndpointProps) => { }: IAddNotificationEndpointProps) => {
//Local States //Local States
const [valuesObj, setValueObj] = useState<IElementValue[]>([]); const [valuesObj, setValueObj] = useState<IElementValue[]>([]);
@@ -75,11 +77,11 @@ const EditConfiguration = ({
if (configId) { if (configId) {
api api
.invoke("GET", `/api/v1/configs/${configId}`) .invoke("GET", `/api/v1/configs/${configId}`)
.then(res => { .then((res) => {
const keyVals = get(res, "key_values", []); const keyVals = get(res, "key_values", []);
setConfigValues(keyVals); setConfigValues(keyVals);
}) })
.catch(err => { .catch((err) => {
setLoadingConfig(false); setLoadingConfig(false);
setErrorConfig(err); setErrorConfig(err);
}); });
@@ -90,7 +92,7 @@ const EditConfiguration = ({
useEffect(() => { useEffect(() => {
if (saving) { if (saving) {
const payload = { const payload = {
key_values: removeEmptyFields(valuesObj) key_values: removeEmptyFields(valuesObj),
}; };
api api
.invoke( .invoke(
@@ -98,14 +100,14 @@ const EditConfiguration = ({
`/api/v1/configs/${selectedConfiguration.configuration_id}`, `/api/v1/configs/${selectedConfiguration.configuration_id}`,
payload payload
) )
.then(res => { .then((res) => {
setSaving(false); setSaving(false);
setErrorConfig(""); setErrorConfig("");
serverNeedsRestart(true); serverNeedsRestart(true);
closeModalAndRefresh(); closeModalAndRefresh();
}) })
.catch(err => { .catch((err) => {
setSaving(false); setSaving(false);
setErrorConfig(err); setErrorConfig(err);
}); });
@@ -115,7 +117,7 @@ const EditConfiguration = ({
serverNeedsRestart, serverNeedsRestart,
selectedConfiguration, selectedConfiguration,
valuesObj, valuesObj,
closeModalAndRefresh closeModalAndRefresh,
]); ]);
//Fetch Actions //Fetch Actions
@@ -125,7 +127,7 @@ const EditConfiguration = ({
}; };
const onValueChange = useCallback( const onValueChange = useCallback(
newValue => { (newValue) => {
setValueObj(newValue); setValueObj(newValue);
}, },
[setValueObj] [setValueObj]
@@ -157,12 +159,11 @@ const EditConfiguration = ({
onChange={onValueChange} onChange={onValueChange}
defaultVals={configValues} defaultVals={configValues}
/> />
<Grid item xs={3} className={classes.buttonContainer}> <Grid item xs={12} className={classes.buttonContainer}>
<Button <Button
type="submit" type="submit"
variant="contained" variant="contained"
color="primary" color="primary"
fullWidth
disabled={saving} disabled={saving}
> >
Save Save

View File

@@ -20,7 +20,7 @@ import {
createStyles, createStyles,
StyledProps, StyledProps,
Theme, Theme,
withStyles withStyles,
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import CssBaseline from "@material-ui/core/CssBaseline"; import CssBaseline from "@material-ui/core/CssBaseline";
import Drawer from "@material-ui/core/Drawer"; import Drawer from "@material-ui/core/Drawer";
@@ -36,14 +36,14 @@ import {
RouteComponentProps, RouteComponentProps,
Router, Router,
Switch, Switch,
withRouter withRouter,
} from "react-router-dom"; } from "react-router-dom";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { AppState } from "../../store"; import { AppState } from "../../store";
import { import {
serverIsLoading, serverIsLoading,
serverNeedsRestart, serverNeedsRestart,
setMenuOpen setMenuOpen,
} from "../../actions"; } from "../../actions";
import { ThemedComponentProps } from "@material-ui/core/styles/withTheme"; import { ThemedComponentProps } from "@material-ui/core/styles/withTheme";
import Buckets from "./Buckets/Buckets"; import Buckets from "./Buckets/Buckets";
@@ -63,6 +63,7 @@ import { Button, LinearProgress } from "@material-ui/core";
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel"; import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
import Trace from "./Trace/Trace"; import Trace from "./Trace/Trace";
import Logs from "./Logs/Logs"; import Logs from "./Logs/Logs";
import Watch from "./Watch/Watch";
function Copyright() { function Copyright() {
return ( return (
@@ -82,43 +83,43 @@ const drawerWidth = 254;
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
root: { root: {
display: "flex" display: "flex",
}, },
toolbar: { toolbar: {
background: theme.palette.background.default, background: theme.palette.background.default,
color: "black", color: "black",
paddingRight: 24 // keep right padding when drawer closed paddingRight: 24, // keep right padding when drawer closed
}, },
toolbarIcon: { toolbarIcon: {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "flex-end", justifyContent: "flex-end",
padding: "0 8px", padding: "0 8px",
...theme.mixins.toolbar ...theme.mixins.toolbar,
}, },
appBar: { appBar: {
zIndex: theme.zIndex.drawer + 1, zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], { transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen duration: theme.transitions.duration.leavingScreen,
}) }),
}, },
appBarShift: { appBarShift: {
marginLeft: drawerWidth, marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`, width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(["width", "margin"], { transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen duration: theme.transitions.duration.enteringScreen,
}) }),
}, },
menuButton: { menuButton: {
marginRight: 36 marginRight: 36,
}, },
menuButtonHidden: { menuButtonHidden: {
display: "none" display: "none",
}, },
title: { title: {
flexGrow: 1 flexGrow: 1,
}, },
drawerPaper: { drawerPaper: {
position: "relative", position: "relative",
@@ -126,40 +127,40 @@ const styles = (theme: Theme) =>
width: drawerWidth, width: drawerWidth,
transition: theme.transitions.create("width", { transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen duration: theme.transitions.duration.enteringScreen,
}) }),
}, },
drawerPaperClose: { drawerPaperClose: {
overflowX: "hidden", overflowX: "hidden",
transition: theme.transitions.create("width", { transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen duration: theme.transitions.duration.leavingScreen,
}), }),
width: theme.spacing(7), width: theme.spacing(7),
[theme.breakpoints.up("sm")]: { [theme.breakpoints.up("sm")]: {
width: theme.spacing(9) width: theme.spacing(9),
} },
}, },
appBarSpacer: { appBarSpacer: {
height: "5px" height: "5px",
}, },
content: { content: {
flexGrow: 1, flexGrow: 1,
height: "100vh", height: "100vh",
overflow: "auto" overflow: "auto",
}, },
container: { container: {
paddingTop: theme.spacing(4), paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4) paddingBottom: theme.spacing(4),
}, },
paper: { paper: {
padding: theme.spacing(2), padding: theme.spacing(2),
display: "flex", display: "flex",
overflow: "auto", overflow: "auto",
flexDirection: "column" flexDirection: "column",
}, },
fixedHeight: { fixedHeight: {
minHeight: 240 minHeight: 240,
}, },
warningBar: { warningBar: {
background: theme.palette.primary.main, background: theme.palette.primary.main,
@@ -167,20 +168,20 @@ const styles = (theme: Theme) =>
heigh: "60px", heigh: "60px",
widht: "100%", widht: "100%",
lineHeight: "60px", lineHeight: "60px",
textAlign: "center" textAlign: "center",
} },
}); });
const mapState = (state: AppState) => ({ const mapState = (state: AppState) => ({
open: state.system.sidebarOpen, open: state.system.sidebarOpen,
needsRestart: state.system.serverNeedsRestart, needsRestart: state.system.serverNeedsRestart,
isServerLoading: state.system.serverIsLoading isServerLoading: state.system.serverIsLoading,
}); });
const connector = connect(mapState, { const connector = connect(mapState, {
setMenuOpen, setMenuOpen,
serverNeedsRestart, serverNeedsRestart,
serverIsLoading serverIsLoading,
}); });
interface IConsoleProps { interface IConsoleProps {
@@ -196,14 +197,14 @@ interface IConsoleProps {
class Console extends React.Component< class Console extends React.Component<
IConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps IConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps
> { > {
componentDidMount(): void { componentDidMount(): void {
api api
.invoke("GET", `/api/v1/session`) .invoke("GET", `/api/v1/session`)
.then(res => { .then((res) => {
console.log(res); console.log(res);
}) })
.catch(err => { .catch((err) => {
storage.removeItem("token"); storage.removeItem("token");
history.push("/"); history.push("/");
}); });
@@ -213,13 +214,13 @@ class Console extends React.Component<
this.props.serverIsLoading(true); this.props.serverIsLoading(true);
api api
.invoke("POST", "/api/v1/service/restart", {}) .invoke("POST", "/api/v1/service/restart", {})
.then(res => { .then((res) => {
console.log("success restarting service"); console.log("success restarting service");
console.log(res); console.log(res);
this.props.serverIsLoading(false); this.props.serverIsLoading(false);
this.props.serverNeedsRestart(false); this.props.serverNeedsRestart(false);
}) })
.catch(err => { .catch((err) => {
this.props.serverIsLoading(false); this.props.serverIsLoading(false);
console.log("failure restarting service"); console.log("failure restarting service");
console.log(err); console.log(err);
@@ -234,7 +235,7 @@ class Console extends React.Component<
<Drawer <Drawer
variant="permanent" variant="permanent"
classes={{ classes={{
paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose) paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose),
}} }}
open={open} open={open}
> >
@@ -261,20 +262,20 @@ class Console extends React.Component<
<LinearProgress /> <LinearProgress />
</React.Fragment> </React.Fragment>
) : ( ) : (
<React.Fragment> <React.Fragment>
The instance needs to be restarted for configuration changes The instance needs to be restarted for configuration changes
to take effect.{" "} to take effect.{" "}
<Button <Button
color="secondary" color="secondary"
size="small" size="small"
onClick={() => { onClick={() => {
this.restartServer(); this.restartServer();
}} }}
> >
Restart Restart
</Button> </Button>
</React.Fragment> </React.Fragment>
)} )}
</div> </div>
)} )}
<div className={classes.appBarSpacer} /> <div className={classes.appBarSpacer} />
@@ -306,6 +307,7 @@ class Console extends React.Component<
<Route exact path="/webhook/audit" component={WebhookPanel} /> <Route exact path="/webhook/audit" component={WebhookPanel} />
<Route exct path="/trace" component={Trace} /> <Route exct path="/trace" component={Trace} />
<Route exct path="/logs" component={Logs} /> <Route exct path="/logs" component={Logs} />
<Route exct path="/watch" component={Watch} />
<Route exact path="/"> <Route exact path="/">
<Redirect to="/dashboard" /> <Redirect to="/dashboard" />
</Route> </Route>

View File

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

View File

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

View File

@@ -15,15 +15,13 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket"; import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import storage from "local-storage-fallback";
import { AppState } from "../../../store"; import { AppState } from "../../../store";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { logMessageReceived, logResetMessages } from "./actions"; import { logMessageReceived, logResetMessages } from "./actions";
import { LogMessage } from "./types"; import { LogMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { niceBytes } from "../../../common/utils"; import { timeFromDate } from "../../../common/utils";
import Ansi from "ansi-to-react"; import { isNullOrUndefined } from "util";
import { isNull, isNullOrUndefined } from "util";
import { wsProtocol } from "../../../utils/wsUtils"; import { wsProtocol } from "../../../utils/wsUtils";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@@ -34,31 +32,28 @@ const styles = (theme: Theme) =>
overflow: "auto", overflow: "auto",
"& ul": { "& ul": {
margin: "4px", margin: "4px",
padding: "0px" padding: "0px",
}, },
"& ul li": { "& ul li": {
listStyle: "none", listStyle: "none",
margin: "0px", margin: "0px",
padding: "0px", padding: "0px",
borderBottom: "1px solid #dedede" borderBottom: "1px solid #dedede",
} },
}, },
tab: { tab: {
padding: "25px" padding: "25px",
},
ansiblue: {
color: "blue"
}, },
logerror: { logerror: {
color: "red" color: "#A52A2A",
}, },
logerror_tab: { logerror_tab: {
color: "red", color: "#A52A2A",
padding: "25px" padding: "25px",
}, },
ansidefault: { ansidefault: {
color: "black" color: "black",
} },
}); });
interface ILogs { interface ILogs {
@@ -72,7 +67,7 @@ const Logs = ({
classes, classes,
logMessageReceived, logMessageReceived,
logResetMessages, logResetMessages,
messages messages,
}: ILogs) => { }: ILogs) => {
useEffect(() => { useEffect(() => {
logResetMessages(); logResetMessages();
@@ -114,14 +109,6 @@ const Logs = ({
} }
}, [logMessageReceived]); }, [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 // replaces a character of a string with other at a given index
const replaceWeirdChar = ( const replaceWeirdChar = (
origString: string, origString: string,
@@ -135,23 +122,6 @@ const Logs = ({
return newString; 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) => { const renderError = (logElement: LogMessage) => {
let errorElems = []; let errorElems = [];
if (!isNullOrUndefined(logElement.error)) { if (!isNullOrUndefined(logElement.error)) {
@@ -166,7 +136,7 @@ const Logs = ({
errorElems.push( errorElems.push(
<li key={`time-${logElement.key}`}> <li key={`time-${logElement.key}`}>
<span className={classes.logerror}> <span className={classes.logerror}>
Time: {timeFromdate(logElement.time)} Time: {timeFromDate(logElement.time)}
</span> </span>
</li> </li>
); );
@@ -241,36 +211,22 @@ const Logs = ({
const renderLog = (logElement: LogMessage) => { const renderLog = (logElement: LogMessage) => {
let logMessage = logElement.ConsoleMsg; let logMessage = logElement.ConsoleMsg;
// if somehow after the color code starts with unicode 10 = Line feed // remove any non ascii characters, exclude any control codes
// delete it logMessage = logMessage.replace(/([^\x20-\x7F])/g, "");
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);
}
}
// Select what to add color and what not to. // regex for terminal colors like e.g. `[31;4m `
const colorRegex = /(\[[0-9]+m)(.*?)(\[0+m)/g; const tColorRegex = /((\[[0-9;]+m))/g;
let m = colorRegex.exec(logMessage);
// get substring if there was a match for to split what // get substring if there was a match for to split what
// is going to be colored and what not, here we add color // is going to be colored and what not, here we add color
// only to the first match. // only to the first match.
let substr = logMessage.slice(colorRegex.lastIndex); let substr = logMessage.replace(tColorRegex, "");
substr = substr.replace(regexInit, "");
// 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 starts with multiple spaces add padding
if (strClean.startsWith(" ") || strClean.codePointAt(1) === 32) { if (substr.startsWith(" ")) {
return ( return (
<li key={logElement.key}> <li key={logElement.key}>
<span className={classes.tab}> <span className={classes.tab}>{substr}</span>
{colorify(logMessage)} {substr}
</span>
</li> </li>
); );
} else if (!isNullOrUndefined(logElement.error)) { } else if (!isNullOrUndefined(logElement.error)) {
@@ -280,9 +236,7 @@ const Logs = ({
// for all remaining set default class // for all remaining set default class
return ( return (
<li key={logElement.key}> <li key={logElement.key}>
<span className={classes.ansidefault}> <span className={classes.ansidefault}>{substr}</span>
{colorify(logMessage)} {substr}
</span>
</li> </li>
); );
} }
@@ -293,7 +247,7 @@ const Logs = ({
<h1>Logs</h1> <h1>Logs</h1>
<div className={classes.logList}> <div className={classes.logList}>
<ul> <ul>
{messages.map(m => { {messages.map((m) => {
return renderLog(m); return renderLog(m);
})} })}
</ul> </ul>
@@ -303,12 +257,12 @@ const Logs = ({
}; };
const mapState = (state: AppState) => ({ const mapState = (state: AppState) => ({
messages: state.logs.messages messages: state.logs.messages,
}); });
const connector = connect(mapState, { const connector = connect(mapState, {
logMessageReceived: logMessageReceived, logMessageReceived: logMessageReceived,
logResetMessages: logResetMessages logResetMessages: logResetMessages,
}); });
export default connector(withStyles(styles)(Logs)); export default connector(withStyles(styles)(Logs));

View File

@@ -18,6 +18,7 @@ import React from "react";
import ListItem from "@material-ui/core/ListItem"; import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemIcon from "@material-ui/core/ListItemIcon";
import WebAssetIcon from "@material-ui/icons/WebAsset"; import WebAssetIcon from "@material-ui/icons/WebAsset";
import CenterFocusWeakIcon from '@material-ui/icons/CenterFocusWeak';
import ListItemText from "@material-ui/core/ListItemText"; import ListItemText from "@material-ui/core/ListItemText";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { Divider, Typography, withStyles } from "@material-ui/core"; import { Divider, Typography, withStyles } from "@material-ui/core";
@@ -33,7 +34,7 @@ import {
BucketsIcon, BucketsIcon,
DashboardIcon, DashboardIcon,
PermissionIcon, PermissionIcon,
UsersIcon UsersIcon,
} from "../../icons"; } from "../../icons";
import { createStyles, Theme } from "@material-ui/core/styles"; import { createStyles, Theme } from "@material-ui/core/styles";
import PersonIcon from "@material-ui/icons/Person"; import PersonIcon from "@material-ui/icons/Person";
@@ -49,8 +50,8 @@ const styles = (theme: Theme) =>
marginBottom: "20px", marginBottom: "20px",
textAlign: "center", textAlign: "center",
"& img": { "& img": {
width: "120px" width: "120px",
} },
}, },
menuList: { menuList: {
"& .active": { "& .active": {
@@ -60,31 +61,31 @@ const styles = (theme: Theme) =>
background: background:
"transparent linear-gradient(90deg, #362585 0%, #362585 7%, #281B6F 39%, #1F1661 100%) 0% 0% no-repeat padding-box", "transparent linear-gradient(90deg, #362585 0%, #362585 7%, #281B6F 39%, #1F1661 100%) 0% 0% no-repeat padding-box",
"& .MuiSvgIcon-root": { "& .MuiSvgIcon-root": {
color: "white" color: "white",
} },
}, },
"& .MuiListItem-root": { "& .MuiListItem-root": {
marginTop: "16px" marginTop: "16px",
}, },
paddingLeft: "30px", paddingLeft: "30px",
"& .MuiSvgIcon-root": { "& .MuiSvgIcon-root": {
fontSize: "18px", fontSize: "18px",
color: "#393939" color: "#393939",
}, },
"& .MuiListItemIcon-root": { "& .MuiListItemIcon-root": {
minWidth: "40px" minWidth: "40px",
}, },
"& .MuiTypography-root": { "& .MuiTypography-root": {
fontSize: "14px" fontSize: "14px",
}, },
"& .MuiListItem-gutters": { "& .MuiListItem-gutters": {
paddingRight: "0px" paddingRight: "0px",
} },
} },
}); });
const mapState = (state: AppState) => ({ const mapState = (state: AppState) => ({
open: state.system.loggedIn open: state.system.loggedIn,
}); });
const connector = connect(mapState, { userLoggedIn }); const connector = connect(mapState, { userLoggedIn });
@@ -132,6 +133,12 @@ class Menu extends React.Component<MenuProps> {
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Buckets" /> <ListItemText primary="Buckets" />
</ListItem> </ListItem>
<ListItem button component={NavLink} to="/watch">
<ListItemIcon>
<CenterFocusWeakIcon />
</ListItemIcon>
<ListItemText primary="Watch" />
</ListItem>
<Divider /> <Divider />
<ListItem component={Typography}>Admin</ListItem> <ListItem component={Typography}>Admin</ListItem>
<ListItem button component={NavLink} to="/users"> <ListItem button component={NavLink} to="/users">

View File

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

View File

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

View File

@@ -15,13 +15,12 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket"; import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import storage from "local-storage-fallback";
import { AppState } from "../../../store"; import { AppState } from "../../../store";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { traceMessageReceived, traceResetMessages } from "./actions"; import { traceMessageReceived, traceResetMessages } from "./actions";
import { TraceMessage } from "./types"; import { TraceMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; 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"; import { wsProtocol } from "../../../utils/wsUtils";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@@ -92,14 +91,6 @@ const Trace = ({
} }
}, [traceMessageReceived]); }, [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 ( return (
<div> <div>
<h1>Trace</h1> <h1>Trace</h1>
@@ -108,7 +99,7 @@ const Trace = ({
{messages.map(m => { {messages.map(m => {
return ( return (
<li key={m.key}> <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} {" "} {m.api} {m.host} {m.client} {m.callStats.duration} {" "}
{niceBytes(m.callStats.rx + "")} {" "} {niceBytes(m.callStats.rx + "")} {" "}
{niceBytes(m.callStats.tx + "")} {niceBytes(m.callStats.tx + "")}

View File

@@ -18,6 +18,7 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, LinearProgress } from "@material-ui/core"; import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import api from "../../../common/api"; import api from "../../../common/api";
import GroupsSelectors from "./GroupsSelectors"; import GroupsSelectors from "./GroupsSelectors";
import Title from "../../../common/Title"; import Title from "../../../common/Title";
@@ -33,24 +34,25 @@ interface IAddToGroup {
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
errorBlock: { errorBlock: {
color: "red" color: "red",
}, },
strongText: { strongText: {
fontWeight: 700 fontWeight: 700,
}, },
keyName: { keyName: {
marginLeft: 5 marginLeft: 5,
}, },
buttonContainer: { buttonContainer: {
textAlign: "right" textAlign: "right",
} },
...modalBasic,
}); });
const AddToGroup = ({ const AddToGroup = ({
open, open,
checkedUsers, checkedUsers,
closeModalAndRefresh, closeModalAndRefresh,
classes classes,
}: IAddToGroup) => { }: IAddToGroup) => {
//Local States //Local States
const [saving, isSaving] = useState<boolean>(false); const [saving, isSaving] = useState<boolean>(false);
@@ -64,14 +66,14 @@ const AddToGroup = ({
api api
.invoke("PUT", "/api/v1/users-groups-bulk", { .invoke("PUT", "/api/v1/users-groups-bulk", {
groups: selectedGroups, groups: selectedGroups,
users: checkedUsers users: checkedUsers,
}) })
.then(res => { .then((res) => {
isSaving(false); isSaving(false);
setError(""); setError("");
closeModalAndRefresh(true); closeModalAndRefresh(true);
}) })
.catch(err => { .catch((err) => {
isSaving(false); isSaving(false);
setError(err); setError(err);
}); });
@@ -86,7 +88,7 @@ const AddToGroup = ({
setError, setError,
closeModalAndRefresh, closeModalAndRefresh,
selectedGroups, selectedGroups,
checkedUsers checkedUsers,
]); ]);
//Fetch Actions //Fetch Actions
@@ -106,35 +108,34 @@ const AddToGroup = ({
> >
<form noValidate autoComplete="off" onSubmit={setSaving}> <form noValidate autoComplete="off" onSubmit={setSaving}>
<Grid container> <Grid container>
{updatingError !== "" && ( <Grid item xs={12} className={classes.formScrollable}>
<Grid item xs={12}> {updatingError !== "" && (
<Typography <Grid item xs={12}>
component="p" <Typography
variant="body1" component="p"
className={classes.errorBlock} variant="body1"
> className={classes.errorBlock}
{updatingError} >
</Typography> {updatingError}
</Grid> </Typography>
)} </Grid>
)}
<Grid item xs={12}> <Grid item xs={12}>
<Title>Users to be altered</Title> <Title>Users to be altered</Title>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
{checkedUsers.join(", ")} {checkedUsers.join(", ")}
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<br /> <br />
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<GroupsSelectors <GroupsSelectors
selectedGroups={selectedGroups} selectedGroups={selectedGroups}
setSelectedGroups={setSelectedGroups} setSelectedGroups={setSelectedGroups}
/> />
</Grid> </Grid>
<Grid item xs={12}>
<br />
</Grid> </Grid>
<Grid item xs={12} className={classes.buttonContainer}> <Grid item xs={12} className={classes.buttonContainer}>
<Button <Button

View File

@@ -19,8 +19,9 @@ import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import { Button, LinearProgress } from "@material-ui/core"; import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; 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 { User } from "./types";
import api from "../../../common/api";
import GroupsSelectors from "./GroupsSelectors"; import GroupsSelectors from "./GroupsSelectors";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper"; import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
@@ -29,17 +30,18 @@ import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/Radi
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
errorBlock: { errorBlock: {
color: "red" color: "red",
}, },
strongText: { strongText: {
fontWeight: 700 fontWeight: 700,
}, },
keyName: { keyName: {
marginLeft: 5 marginLeft: 5,
}, },
buttonContainer: { buttonContainer: {
textAlign: "right" textAlign: "right",
} },
...modalBasic,
}); });
interface IAddUserContentProps { interface IAddUserContentProps {
@@ -68,7 +70,7 @@ class AddUserContent extends React.Component<
accessKey: "", accessKey: "",
secretKey: "", secretKey: "",
enabled: "enabled", enabled: "enabled",
selectedGroups: [] selectedGroups: [],
}; };
componentDidMount(): void { componentDidMount(): void {
@@ -77,7 +79,7 @@ class AddUserContent extends React.Component<
this.setState({ this.setState({
accessKey: "", accessKey: "",
secretKey: "", secretKey: "",
selectedGroups: [] selectedGroups: [],
}); });
} else { } else {
this.getUserInformation(); this.getUserInformation();
@@ -91,7 +93,7 @@ class AddUserContent extends React.Component<
addLoading, addLoading,
secretKey, secretKey,
selectedGroups, selectedGroups,
enabled enabled,
} = this.state; } = this.state;
const { selectedUser } = this.props; const { selectedUser } = this.props;
if (addLoading) { if (addLoading) {
@@ -102,23 +104,23 @@ class AddUserContent extends React.Component<
api api
.invoke("PUT", `/api/v1/users/${selectedUser.accessKey}`, { .invoke("PUT", `/api/v1/users/${selectedUser.accessKey}`, {
status: enabled, status: enabled,
groups: selectedGroups groups: selectedGroups,
}) })
.then(res => { .then((res) => {
this.setState( this.setState(
{ {
addLoading: false, addLoading: false,
addError: "" addError: "",
}, },
() => { () => {
this.props.closeModalAndRefresh(); this.props.closeModalAndRefresh();
} }
); );
}) })
.catch(err => { .catch((err) => {
this.setState({ this.setState({
addLoading: false, addLoading: false,
addError: err addError: err,
}); });
}); });
} else { } else {
@@ -126,24 +128,24 @@ class AddUserContent extends React.Component<
.invoke("POST", "/api/v1/users", { .invoke("POST", "/api/v1/users", {
accessKey, accessKey,
secretKey, secretKey,
groups: selectedGroups groups: selectedGroups,
}) })
.then(res => { .then((res) => {
this.setState( this.setState(
{ {
addLoading: false, addLoading: false,
addError: "" addError: "",
}, },
() => { () => {
this.props.closeModalAndRefresh(); this.props.closeModalAndRefresh();
} }
); );
}) })
.catch(err => { .catch((err) => {
console.log(err); console.log(err);
this.setState({ this.setState({
addLoading: false, addLoading: false,
addError: err addError: err,
}); });
}); });
} }
@@ -159,19 +161,19 @@ class AddUserContent extends React.Component<
api api
.invoke("GET", `/api/v1/users/${selectedUser.accessKey}`) .invoke("GET", `/api/v1/users/${selectedUser.accessKey}`)
.then(res => { .then((res) => {
this.setState({ this.setState({
addLoading: false, addLoading: false,
addError: "", addError: "",
accessKey: res.accessKey, accessKey: res.accessKey,
selectedGroups: res.memberOf || [], selectedGroups: res.memberOf || [],
enabled: res.status enabled: res.status,
}); });
}) })
.catch(err => { .catch((err) => {
this.setState({ this.setState({
addLoading: false, addLoading: false,
addError: err addError: err,
}); });
}); });
} }
@@ -184,7 +186,7 @@ class AddUserContent extends React.Component<
accessKey, accessKey,
secretKey, secretKey,
selectedGroups, selectedGroups,
enabled enabled,
} = this.state; } = this.state;
return ( return (
@@ -204,68 +206,67 @@ class AddUserContent extends React.Component<
}} }}
> >
<Grid container> <Grid container>
{addError !== "" && ( <Grid item xs={12} className={classes.formScrollable}>
<Grid item xs={12}> {addError !== "" && (
<Typography <Grid item xs={12}>
component="p" <Typography
variant="body1" component="p"
className={classes.errorBlock} variant="body1"
> className={classes.errorBlock}
{addError} >
</Typography> {addError}
</Grid> </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 <InputBoxWrapper
id="standard-multiline-static" id="accesskey-input"
name="standard-multiline-static" name="accesskey-input"
label="Secret Key" label="Access Key"
type="password" value={accessKey}
value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 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}> {selectedUser !== null ? (
<GroupsSelectors <RadioGroupSelector
selectedGroups={selectedGroups} currentSelection={enabled}
setSelectedGroups={(elements: string[]) => { id="user-status"
this.setState({ name="user-status"
selectedGroups: elements label="Status"
}); onChange={(e) => {
}} this.setState({ enabled: e.target.value });
/> }}
</Grid> selectorOptions={[
<Grid item xs={12}> { label: "Enabled", value: "enabled" },
<br /> { 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>
<Grid item xs={12} className={classes.buttonContainer}> <Grid item xs={12} className={classes.buttonContainer}>
<Button <Button

View 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));

View 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
};
}

View File

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

View 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;
}

View File

@@ -19,11 +19,13 @@ import thunk from "redux-thunk";
import { systemReducer } from "./reducer"; import { systemReducer } from "./reducer";
import { traceReducer } from "./screens/Console/Trace/reducers"; import { traceReducer } from "./screens/Console/Trace/reducers";
import { logReducer } from "./screens/Console/Logs/reducers"; import { logReducer } from "./screens/Console/Logs/reducers";
import { watchReducer } from "./screens/Console/Watch/reducers";
const globalReducer = combineReducers({ const globalReducer = combineReducers({
system: systemReducer, system: systemReducer,
trace: traceReducer, trace: traceReducer,
logs: logReducer logs: logReducer,
watch: watchReducer,
}); });
declare global { declare global {

View File

@@ -15,7 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
export const wsProtocol = (protocol: string): string => { export const wsProtocol = (protocol: string): string => {
let wsProtocol = "ws"; let wsProtocol = "ws";
if (protocol == "https:") { if (protocol === "https:") {
wsProtocol = "wss"; wsProtocol = "wss";
} }
return wsProtocol; return wsProtocol;

View File

@@ -39,7 +39,6 @@ func TestAdminConsoleLog(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
adminClient := adminClientMock{} adminClient := adminClientMock{}
mockWSConn := mockConn{} mockWSConn := mockConn{}
wsClientMock := wsClientMock{madmin: adminClient}
function := "startConsoleLog()" function := "startConsoleLog()"
testReceiver := make(chan madmin.LogInfo, 5) testReceiver := make(chan madmin.LogInfo, 5)
@@ -83,7 +82,7 @@ func TestAdminConsoleLog(t *testing.T) {
writesCount++ writesCount++
return nil 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()) t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
} }
// check that the TestReceiver got the same number of data from Console. // 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 { connWriteMessageMock = func(messageType int, data []byte) error {
return fmt.Errorf("error on write") 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()) assert.Equal("error on write", err.Error())
} }
@@ -107,7 +106,7 @@ func TestAdminConsoleLog(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""} 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()) 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) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""} 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()) 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) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""} 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()) 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) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, fmt.Errorf("error on read") 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()) assert.Equal("error on read", err.Error())
} }
@@ -162,7 +161,7 @@ func TestAdminConsoleLog(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, nil 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()) assert.Equal("error on Console", err.Error())
} }
} }

View File

@@ -40,7 +40,6 @@ func TestAdminTrace(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
adminClient := adminClientMock{} adminClient := adminClientMock{}
mockWSConn := mockConn{} mockWSConn := mockConn{}
wsClientMock := wsClientMock{madmin: adminClient}
function := "startTraceInfo()" function := "startTraceInfo()"
testReceiver := make(chan shortTraceMsg, 5) testReceiver := make(chan shortTraceMsg, 5)
@@ -84,7 +83,7 @@ func TestAdminTrace(t *testing.T) {
writesCount++ writesCount++
return nil 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()) t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
} }
// check that the TestReceiver got the same number of data from trace. // 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 { connWriteMessageMock = func(messageType int, data []byte) error {
return fmt.Errorf("error on write") 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()) assert.Equal("error on write", err.Error())
} }
@@ -108,7 +107,7 @@ func TestAdminTrace(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""} 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()) 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) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""} 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()) 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) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""} 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()) 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) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, fmt.Errorf("error on read") 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()) assert.Equal("error on read", err.Error())
} }
@@ -163,7 +162,7 @@ func TestAdminTrace(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, nil 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()) assert.Equal("error on trace", err.Error())
} }
} }

View File

@@ -44,7 +44,9 @@ func NewAdminClient(url, accessKey, secretKey string) (*madmin.AdminClient, *pro
AppName: appName, AppName: appName,
AppVersion: McsVersion, AppVersion: McsVersion,
AppComments: []string{appName, runtime.GOOS, runtime.GOARCH}, AppComments: []string{appName, runtime.GOOS, runtime.GOARCH},
Insecure: false,
}) })
s3Client.SetCustomTransport(STSClient.Transport)
if err != nil { if err != nil {
return nil, err.Trace(url) 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 // newAdminFromClaims creates a minio admin from Decrypted claims using Assume role credentials
func newAdminFromClaims(claims *auth.DecryptedClaims) (*madmin.AdminClient, error) { func newAdminFromClaims(claims *auth.DecryptedClaims) (*madmin.AdminClient, error) {
tlsEnabled := getMinIOEndpointIsSecure()
adminClient, err := madmin.NewWithOptions(getMinIOEndpoint(), &madmin.Options{ adminClient, err := madmin.NewWithOptions(getMinIOEndpoint(), &madmin.Options{
Creds: credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken), Creds: credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken),
Secure: getMinIOEndpointIsSecure(), Secure: tlsEnabled,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
adminClient.SetCustomTransport(STSClient.Transport)
return adminClient, nil return adminClient, nil
} }

View File

@@ -19,11 +19,15 @@ package restapi
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"errors"
mc "github.com/minio/mc/cmd" mc "github.com/minio/mc/cmd"
"github.com/minio/mc/pkg/probe" "github.com/minio/mc/pkg/probe"
"github.com/minio/mcs/pkg/auth" "github.com/minio/mcs/pkg/auth"
xjwt "github.com/minio/mcs/pkg/auth/jwt" 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"
"github.com/minio/minio-go/v6/pkg/credentials" "github.com/minio/minio-go/v6/pkg/credentials"
) )
@@ -90,6 +94,7 @@ func (c minioClient) getBucketPolicy(bucketName string) (string, error) {
type MCS3Client interface { type MCS3Client interface {
addNotificationConfig(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error addNotificationConfig(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error
removeNotificationConfig(arn string, event string, prefix string, suffix string) *probe.Error removeNotificationConfig(arn string, event string, prefix string, suffix string) *probe.Error
watch(options mc.WatchOptions) (*mc.WatchObject, *probe.Error)
} }
// Interface implementation // Interface implementation
@@ -110,6 +115,10 @@ func (c mcS3Client) removeNotificationConfig(arn string, event string, prefix st
return c.client.RemoveNotificationConfig(arn, event, prefix, suffix) 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 // MCSCredentials interface with all functions to be implemented
// by mock when testing, it should include all needed minioCredentials.Credentials api calls // by mock when testing, it should include all needed minioCredentials.Credentials api calls
// that are used within this project. // that are used within this project.
@@ -133,13 +142,62 @@ func (c mcsCredentials) Expire() {
c.minioCredentials.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) { func newMcsCredentials(accessKey, secretKey, location string) (*credentials.Credentials, error) {
return credentials.NewSTSAssumeRole(getMinIOServer(), credentials.STSAssumeRoleOptions{ mcsEndpoint := getMinIOServer()
AccessKey: accessKey, if mcsEndpoint == "" {
SecretKey: secretKey, return nil, errors.New("STS endpoint cannot be empty")
Location: location, }
DurationSeconds: xjwt.GetMcsSTSAndJWTDurationInSeconds(), 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 // getMcsCredentialsFromJWT returns the *minioCredentials.Credentials associated to the
@@ -160,30 +218,35 @@ func newMinioClient(jwt string) (*minio.Client, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
adminClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{ minioClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{
Creds: creds, Creds: creds,
Secure: getMinIOEndpointIsSecure(), Secure: getMinIOEndpointIsSecure(),
}) })
if err != nil { if err != nil {
return nil, err 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 // 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() endpoint := getMinIOServer()
accessKeyID := getAccessKey()
secretAccessKey := getSecretKey()
useSSL := getMinIOEndpointIsSecure() useSSL := getMinIOEndpointIsSecure()
if bucketName != nil { claims, err := auth.JWTAuthenticate(jwt)
endpoint += fmt.Sprintf("/%s", *bucketName)
}
s3Config := newS3Config(endpoint, accessKeyID, secretAccessKey, !useSSL)
client, err := mc.S3New(s3Config)
if err != nil { 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) s3Client, ok := client.(*mc.S3Client)
if !ok { if !ok {
@@ -195,7 +258,7 @@ func newS3BucketClient(bucketName *string) (*mc.S3Client, error) {
// newS3Config simply creates a new Config struct using the passed // newS3Config simply creates a new Config struct using the passed
// parameters. // 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 // We have a valid alias and hostConfig. We populate the
// minioCredentials from the match found in the config file. // minioCredentials from the match found in the config file.
s3Config := new(mc.Config) s3Config := new(mc.Config)
@@ -209,6 +272,7 @@ func newS3Config(endpoint, accessKey, secretKey string, isSecure bool) *mc.Confi
s3Config.HostURL = endpoint s3Config.HostURL = endpoint
s3Config.AccessKey = accessKey s3Config.AccessKey = accessKey
s3Config.SecretKey = secretKey s3Config.SecretKey = secretKey
s3Config.SessionToken = sessionToken
s3Config.Signature = "S3v4" s3Config.Signature = "S3v4"
return s3Config return s3Config
} }

View File

@@ -48,7 +48,17 @@ func getSecretKey() string {
} }
func getMinIOServer() 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 { func getMinIOEndpoint() string {
@@ -67,7 +77,7 @@ func getMinIOEndpointIsSecure() bool {
if strings.Contains(server, "://") { if strings.Contains(server, "://") {
parts := strings.Split(server, "://") parts := strings.Split(server, "://")
if len(parts) > 1 { if len(parts) > 1 {
if parts[1] == "https" { if parts[0] == "https" {
return true return true
} }
} }

View File

@@ -18,15 +18,16 @@ package restapi
const ( const (
// consts for common configuration // consts for common configuration
McsVersion = `0.1.0` McsVersion = `0.1.0`
McsAccessKey = "MCS_ACCESS_KEY" McsAccessKey = "MCS_ACCESS_KEY"
McsSecretKey = "MCS_SECRET_KEY" McsSecretKey = "MCS_SECRET_KEY"
McsMinIOServer = "MCS_MINIO_SERVER" McsMinIOServer = "MCS_MINIO_SERVER"
McsProductionMode = "MCS_PRODUCTION_MODE" McsMinIOServerTLSRootCAs = "MCS_MINIO_SERVER_TLS_ROOT_CAS"
McsHostname = "MCS_HOSTNAME" McsProductionMode = "MCS_PRODUCTION_MODE"
McsPort = "MCS_PORT" McsHostname = "MCS_HOSTNAME"
McsTLSHostname = "MCS_TLS_HOSTNAME" McsPort = "MCS_PORT"
McsTLSPort = "MCS_TLS_PORT" McsTLSHostname = "MCS_TLS_HOSTNAME"
McsTLSPort = "MCS_TLS_PORT"
// consts for Secure middleware // consts for Secure middleware
McsSecureAllowedHosts = "MCS_SECURE_ALLOWED_HOSTS" McsSecureAllowedHosts = "MCS_SECURE_ALLOWED_HOSTS"

95
restapi/tls.go Normal file
View 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,
}
}

View File

@@ -40,14 +40,16 @@ func registerBucketEventsHandlers(api *operations.McsAPI) {
}) })
// create bucket event // create bucket event
api.UserAPICreateBucketEventHandler = user_api.CreateBucketEventHandlerFunc(func(params user_api.CreateBucketEventParams, principal *models.Principal) middleware.Responder { 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.NewCreateBucketEventDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
} }
return user_api.NewCreateBucketEventCreated() return user_api.NewCreateBucketEventCreated()
}) })
// delete bucket event // delete bucket event
api.UserAPIDeleteBucketEventHandler = user_api.DeleteBucketEventHandlerFunc(func(params user_api.DeleteBucketEventParams, principal *models.Principal) middleware.Responder { 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.NewDeleteBucketEventDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
} }
return user_api.NewDeleteBucketEventNoContent() 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 // getCreateBucketEventsResponse calls createBucketEvent to add a bucket event notification
func getCreateBucketEventsResponse(bucketName string, eventReq *models.BucketEventRequest) error { func getCreateBucketEventsResponse(sessionID, bucketName string, eventReq *models.BucketEventRequest) error {
s3Client, err := newS3BucketClient(swag.String(bucketName)) s3Client, err := newS3BucketClient(sessionID, bucketName)
if err != nil { if err != nil {
log.Println("error creating S3Client:", err) log.Println("error creating S3Client:", err)
return err return err
@@ -214,8 +216,8 @@ func joinNotificationEvents(events []models.NotificationEventType) string {
} }
// getDeleteBucketEventsResponse calls deleteBucketEventNotification() to delete a bucket event notification // getDeleteBucketEventsResponse calls deleteBucketEventNotification() to delete a bucket event notification
func getDeleteBucketEventsResponse(bucketName string, arn string, events []models.NotificationEventType, prefix, suffix *string) error { func getDeleteBucketEventsResponse(sessionID, bucketName string, arn string, events []models.NotificationEventType, prefix, suffix *string) error {
s3Client, err := newS3BucketClient(swag.String(bucketName)) s3Client, err := newS3BucketClient(sessionID, bucketName)
if err != nil { if err != nil {
log.Println("error creating S3Client:", err) log.Println("error creating S3Client:", err)
return err return err

View File

@@ -32,7 +32,8 @@ import (
) )
var ( 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) { 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 { api.UserAPILoginHandler = user_api.LoginHandlerFunc(func(params user_api.LoginParams) middleware.Responder {
loginResponse, err := getLoginResponse(params.Body) loginResponse, err := getLoginResponse(params.Body)
if err != nil { 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) return user_api.NewLoginCreated().WithPayload(loginResponse)
}) })
api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder { api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
loginResponse, err := getLoginOauth2AuthResponse(params.Body) loginResponse, err := getLoginOauth2AuthResponse(params.Body)
if err != nil { 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) return user_api.NewLoginOauth2AuthCreated().WithPayload(loginResponse)
}) })
} }
var errInvalidCredentials = errors.New("invalid minioCredentials")
// login performs a check of minioCredentials against MinIO // login performs a check of minioCredentials against MinIO
func login(credentials MCSCredentials) (*string, error) { func login(credentials MCSCredentials) (*string, error) {
// try to obtain minioCredentials, // try to obtain minioCredentials,
tokens, err := credentials.Get() tokens, err := credentials.Get()
if err != nil { if err != nil {
log.Println("error authenticating user", err)
return nil, errInvalidCredentials return nil, errInvalidCredentials
} }
// if we made it here, the minioCredentials work, generate a jwt with claims // if we made it here, the minioCredentials work, generate a jwt with claims
jwt, err := auth.NewJWTWithClaimsForClient(&tokens, getMinIOServer()) jwt, err := auth.NewJWTWithClaimsForClient(&tokens, getMinIOServer())
if err != nil { if err != nil {
log.Println("error authenticating user", err)
return nil, errInvalidCredentials return nil, errInvalidCredentials
} }
return &jwt, nil return &jwt, nil
} }
func getConfiguredRegion(client MinioAdmin) string { func getConfiguredRegionForLogin(client MinioAdmin) (string, error) {
location := "" location := ""
configuration, err := getConfig(client, "region") configuration, err := getConfig(client, "region")
if err != nil { if err != nil {
log.Println("error obtaining MinIO region:", err) log.Println("error obtaining MinIO region:", err)
return location return location, errorGeneric
} }
// region is an array of 1 element // region is an array of 1 element
if len(configuration) > 0 { if len(configuration) > 0 {
location = configuration[0].Value location = configuration[0].Value
} }
return location return location, nil
} }
// getLoginResponse performs login() and serializes it to the handler's output // 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} adminClient := adminClient{client: mAdmin}
// obtain the configured MinIO region // obtain the configured MinIO region
// need it for user authentication // 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) creds, err := newMcsCredentials(*lr.AccessKey, *lr.SecretKey, location)
if err != nil { if err != nil {
log.Println("error login:", err) log.Println("error login:", err)
return nil, err return nil, errInvalidCredentials
} }
credentials := mcsCredentials{minioCredentials: creds} credentials := mcsCredentials{minioCredentials: creds}
sessionID, err := login(credentials) sessionID, err := login(credentials)
if err != nil { if err != nil {
log.Println("error login:", err)
return nil, err return nil, err
} }
// serialize output // serialize output
@@ -131,7 +134,8 @@ func getLoginDetailsResponse() (*models.LoginDetails, error) {
// initialize new oauth2 client // initialize new oauth2 client
oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil) oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil)
if err != nil { if err != nil {
return nil, err log.Println("error getting new oauth2 provider client", err)
return nil, errorGeneric
} }
// Validate user against IDP // Validate user against IDP
identityProvider := &auth.IdentityProvider{Client: oauth2Client} 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) { func loginOauth2Auth(ctx context.Context, provider *auth.IdentityProvider, code, state string) (*oauth2.User, error) {
userIdentity, err := provider.VerifyIdentity(ctx, code, state) userIdentity, err := provider.VerifyIdentity(ctx, code, state)
if err != nil { if err != nil {
return nil, err log.Println("error validating user identity against idp:", err)
return nil, errorGeneric
} }
return userIdentity, nil return userIdentity, nil
} }
@@ -166,8 +171,7 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
// Validate user against IDP // Validate user against IDP
identity, err := loginOauth2Auth(ctx, identityProvider, *lr.Code, *lr.State) identity, err := loginOauth2Auth(ctx, identityProvider, *lr.Code, *lr.State)
if err != nil { if err != nil {
log.Println("error validating user identity against idp:", err) return nil, err
return nil, errorGeneric
} }
mAdmin, err := newSuperMAdminClient() mAdmin, err := newSuperMAdminClient()
if err != nil { if err != nil {
@@ -179,7 +183,10 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
secretKey := utils.RandomCharString(32) secretKey := utils.RandomCharString(32)
// obtain the configured MinIO region // obtain the configured MinIO region
// need it for user authentication // need it for user authentication
location := getConfiguredRegion(adminClient) location, err := getConfiguredRegionForLogin(adminClient)
if err != nil {
return nil, err
}
// create user in MinIO // create user in MinIO
if _, err := addUser(ctx, adminClient, &accessKey, &secretKey, []string{}); err != nil { if _, err := addUser(ctx, adminClient, &accessKey, &secretKey, []string{}); err != nil {
log.Println("error adding user:", err) log.Println("error adding user:", err)
@@ -207,8 +214,7 @@ func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.Logi
credentials := mcsCredentials{minioCredentials: creds} credentials := mcsCredentials{minioCredentials: creds}
jwt, err := login(credentials) jwt, err := login(credentials)
if err != nil { if err != nil {
log.Println("error login:", err) return nil, err
return nil, errorGeneric
} }
// serialize output // serialize output
loginResponse := &models.LoginResponse{ loginResponse := &models.LoginResponse{

View File

@@ -100,7 +100,7 @@ func TestLoginOauth2Auth(t *testing.T) {
return nil, errors.New("error") return nil, errors.New("error")
} }
if _, err := loginOauth2Auth(ctx, identityProvider, mockCode, mockState); funcAssert.Error(err) { 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 { for _, tt := range tests {
tt.mock() tt.mock()
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if got := getConfiguredRegion(tt.args.client); got != tt.want { if got, _ := getConfiguredRegionForLogin(tt.args.client); got != tt.want {
t.Errorf("getConfiguredRegion() = %v, want %v", got, tt.want) t.Errorf("getConfiguredRegionForLogin() = %v, want %v", got, tt.want)
} }
}) })
} }

165
restapi/user_watch.go Normal file
View 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
View 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)
}

View File

@@ -16,6 +16,8 @@
package restapi package restapi
import "os"
// DifferenceArrays returns the elements in `a` that aren't in `b`. // DifferenceArrays returns the elements in `a` that aren't in `b`.
func DifferenceArrays(a, b []string) []string { func DifferenceArrays(a, b []string) []string {
mb := make(map[string]struct{}, len(b)) mb := make(map[string]struct{}, len(b))
@@ -54,3 +56,12 @@ func UniqueKeys(a []string) []string {
} }
return list 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()
}

View File

@@ -27,6 +27,7 @@ import (
"github.com/go-openapi/errors" "github.com/go-openapi/errors"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/minio/mcs/pkg/auth"
"github.com/minio/mcs/pkg/ws" "github.com/minio/mcs/pkg/ws"
) )
@@ -46,17 +47,29 @@ const (
maxMessageSize = 512 maxMessageSize = 512
) )
// MCSWebsocket interface of a Websocket Client // MCSWebsocketAdmin interface of a Websocket Client
type MCSWebsocket interface { type MCSWebsocketAdmin interface {
// start trace info from servers
trace() trace()
console()
} }
type wsClient struct { type wsAdminClient struct {
// websocket connection. // websocket connection.
conn wsConn conn wsConn
// MinIO admin Client // 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 // 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 // Websocket communication will be done depending
// on the path. // on the path.
// Request should come like ws://<host>:<port>/ws/<api> // Request should come like ws://<host>:<port>/ws/<api>
//
// TODO: Enable CORS
func serveWS(w http.ResponseWriter, req *http.Request) { 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 // authenticate WS connection with MCS
claims, err := ws.Authenticate(req) claims, err := auth.JWTAuthenticate(*sessionID)
if err != nil { if err != nil {
log.Print("error on ws authentication: ", err) log.Print("error on ws authentication: ", err)
errors.ServeError(w, req, err) errors.ServeError(w, req, errors.New(http.StatusUnauthorized, err.Error()))
return return
} }
@@ -125,28 +142,30 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
return return
} }
// Only start Websocket Interaction after user has been wsPath := strings.TrimPrefix(req.URL.Path, wsBasePath)
// authenticated with MinIO switch {
mAdmin, err := newAdminFromClaims(claims) case wsPath == "/trace":
if err != nil { wsAdminClient, err := newWebSocketAdminClient(conn, claims)
log.Println("error creating Madmin Client:", err) if err != nil {
errors.ServeError(w, req, err) errors.ServeError(w, req, err)
return return
} }
// create a minioClient interface implementation go wsAdminClient.trace()
// defining the client to be used case wsPath == "/console":
adminClient := adminClient{client: mAdmin} wsAdminClient, err := newWebSocketAdminClient(conn, claims)
// create a websocket connection interface implementation if err != nil {
// defining the connection to be used errors.ServeError(w, req, err)
wsConnection := wsConn{conn: conn} return
}
// create websocket client and handle request go wsAdminClient.console()
wsClient := &wsClient{conn: wsConnection, madmin: adminClient} case strings.HasPrefix(wsPath, `/watch`):
switch strings.TrimPrefix(req.URL.Path, wsBasePath) { wOptions := getOptionsFromReq(req)
case "/trace": wsS3Client, err := newWebSocketS3Client(conn, *sessionID, wOptions.BucketName)
go wsClient.trace() if err != nil {
case "/console": errors.ServeError(w, req, err)
go wsClient.console() return
}
go wsS3Client.watch(wOptions)
default: default:
// path not found // path not found
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 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 // wsReadCheck ensures that the client is sending a message
// every `pingWait` seconds. If deadline exceeded or an error // every `pingWait` seconds. If deadline exceeded or an error
// happened this will return, meaning it won't be able to ensure // 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 // trace serves madmin.ServiceTraceInfo
// on a Websocket connection. // on a Websocket connection.
func (wsc *wsClient) trace() { func (wsc *wsAdminClient) trace() {
defer func() { defer func() {
log.Println("trace stopped") log.Println("trace stopped")
// close connection after return // close connection after return
@@ -214,7 +278,7 @@ func (wsc *wsClient) trace() {
}() }()
log.Println("trace started") log.Println("trace started")
err := startTraceInfo(wsc.conn, wsc.madmin) err := startTraceInfo(wsc.conn, wsc.client)
// Send Connection Close Message indicating the Status Code // Send Connection Close Message indicating the Status Code
// see https://tools.ietf.org/html/rfc6455#page-45 // see https://tools.ietf.org/html/rfc6455#page-45
if err != nil { if err != nil {
@@ -237,7 +301,7 @@ func (wsc *wsClient) trace() {
// console serves madmin.GetLogs // console serves madmin.GetLogs
// on a Websocket connection. // on a Websocket connection.
func (wsc *wsClient) console() { func (wsc *wsAdminClient) console() {
defer func() { defer func() {
log.Println("console logs stopped") log.Println("console logs stopped")
// close connection after return // close connection after return
@@ -245,7 +309,36 @@ func (wsc *wsClient) console() {
}() }()
log.Println("console logs started") 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 // Send Connection Close Message indicating the Status Code
// see https://tools.ietf.org/html/rfc6455#page-45 // see https://tools.ietf.org/html/rfc6455#page-45
if err != nil { if err != nil {

View File

@@ -51,18 +51,3 @@ func (c mockConn) setReadDeadline(t time.Time) error {
} }
func (c mockConn) setPongHandler(h func(appData string) 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()
}