Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35c3b53a23 | ||
|
|
6fef30f29d | ||
|
|
acf480fd25 | ||
|
|
8bbc4f0192 | ||
|
|
48e7991f11 | ||
|
|
5e9b0652b0 | ||
|
|
438211199d | ||
|
|
0f77a32656 | ||
|
|
9f3e99ede8 | ||
|
|
a8c07c0969 |
127
DEVELOPMENT.md
Normal file
127
DEVELOPMENT.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# LDAP authentication with MCS
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Run openLDAP with docker.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker run --rm -p 389:389 -p 636:636 --name my-openldap-container --detach osixia/openldap:1.3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the `billy.ldif` file using `ldapadd` command to create a new user and assign it to a group.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cat > billy.ldif << EOF
|
||||||
|
# LDIF fragment to create group branch under root
|
||||||
|
dn: uid=billy,dc=example,dc=org
|
||||||
|
uid: billy
|
||||||
|
cn: billy
|
||||||
|
sn: 3
|
||||||
|
objectClass: top
|
||||||
|
objectClass: posixAccount
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
loginShell: /bin/bash
|
||||||
|
homeDirectory: /home/billy
|
||||||
|
uidNumber: 14583102
|
||||||
|
gidNumber: 14564100
|
||||||
|
userPassword: {SSHA}j3lBh1Seqe4rqF1+NuWmjhvtAni1JC5A
|
||||||
|
mail: billy@example.org
|
||||||
|
gecos: Billy User
|
||||||
|
# Create base group
|
||||||
|
dn: ou=groups,dc=example,dc=org
|
||||||
|
objectclass:organizationalunit
|
||||||
|
ou: groups
|
||||||
|
description: generic groups branch
|
||||||
|
# create mcsAdmin group (this already exists on minio and have a policy of s3::*)
|
||||||
|
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
|
||||||
|
objectClass: top
|
||||||
|
objectClass: posixGroup
|
||||||
|
gidNumber: 678
|
||||||
|
# Assing group to new user
|
||||||
|
dn: cn=mcsAdmin,ou=groups,dc=example,dc=org
|
||||||
|
changetype: modify
|
||||||
|
add: memberuid
|
||||||
|
memberuid: billy
|
||||||
|
EOF
|
||||||
|
|
||||||
|
$ docker cp billy.ldif my-openldap-container:/container/service/slapd/assets/test/billy.ldif
|
||||||
|
$ docker exec my-openldap-container ldapadd -x -D "cn=admin,dc=example,dc=org" -w admin -f /container/service/slapd/assets/test/billy.ldif -H ldap://localhost -ZZ
|
||||||
|
```
|
||||||
|
|
||||||
|
Query the ldap server to check the user billy was created correctly and got assigned to the mcsAdmin group, you should get a list
|
||||||
|
containing ldap users and groups.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Query the ldap server again, this time filtering only for the user `billy`, you should see only 1 record.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b uid=billy,dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change the password for user billy
|
||||||
|
|
||||||
|
Set the new password for `billy` to `minio123` and enter `admin` as the default `LDAP Password`
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker exec -it my-openldap-container /bin/bash
|
||||||
|
# ldappasswd -H ldap://localhost -x -D "cn=admin,dc=example,dc=org" -W -S "uid=billy,dc=example,dc=org"
|
||||||
|
New password:
|
||||||
|
Re-enter new password:
|
||||||
|
Enter LDAP Password:
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add the mcsAdmin policy to user billy on MinIO
|
||||||
|
```
|
||||||
|
$ cat > mcsAdmin.json << EOF
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Action": [
|
||||||
|
"admin:*"
|
||||||
|
],
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Sid": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Action": [
|
||||||
|
"s3:*"
|
||||||
|
],
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::*"
|
||||||
|
],
|
||||||
|
"Sid": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
$ mc admin policy add myminio mcsAdmin mcsAdmin.json
|
||||||
|
$ mc admin policy set myminio mcsAdmin user=billy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run MinIO
|
||||||
|
|
||||||
|
```
|
||||||
|
export MINIO_ACCESS_KEY=minio
|
||||||
|
export MINIO_SECRET_KEY=minio123
|
||||||
|
export MINIO_IDENTITY_LDAP_SERVER_ADDR='localhost:389'
|
||||||
|
export MINIO_IDENTITY_LDAP_USERNAME_FORMAT='uid=%s,dc=example,dc=org'
|
||||||
|
export MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER='(|(objectclass=posixAccount)(uid=%s))'
|
||||||
|
export MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY=on
|
||||||
|
export MINIO_IDENTITY_LDAP_SERVER_INSECURE=on
|
||||||
|
./minio server ~/Data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run MCS
|
||||||
|
|
||||||
|
```
|
||||||
|
export MCS_ACCESS_KEY=minio
|
||||||
|
export MCS_SECRET_KEY=minio123
|
||||||
|
...
|
||||||
|
export MCS_LDAP_ENABLED=on
|
||||||
|
./mcs server
|
||||||
|
```
|
||||||
@@ -68,6 +68,15 @@ export MCS_MINIO_SERVER=http://localhost:9000
|
|||||||
./mcs server
|
./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
4
go.mod
@@ -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
12
go.sum
@@ -12,6 +12,7 @@ github.com/Azure/azure-storage-blob-go v0.8.0 h1:53qhf0Oxa0nOjgbDeeYPUeyiNmafAFE
|
|||||||
github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
|
github.com/Azure/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
39
pkg/auth/ldap.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2020 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v6/pkg/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errInvalidCredentials = errors.New("invalid Credentials")
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetMcsCredentialsFromLDAP authenticates the user against MinIO when the LDAP integration is enabled
|
||||||
|
// if the authentication succeed *credentials.Credentials object is returned and we continue with the normal STSAssumeRole flow
|
||||||
|
func GetMcsCredentialsFromLDAP(endpoint, ldapUser, ldapPassword string) (*credentials.Credentials, error) {
|
||||||
|
creds, err := credentials.NewLDAPIdentity(endpoint, ldapUser, ldapPassword)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("LDAP authentication error: ", err)
|
||||||
|
return nil, errInvalidCredentials
|
||||||
|
}
|
||||||
|
return creds, nil
|
||||||
|
}
|
||||||
27
pkg/auth/ldap/config.go
Normal file
27
pkg/auth/ldap/config.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2020 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ldap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetLDAPEnabled() bool {
|
||||||
|
return strings.ToLower(env.Get(MCSLDAPEnabled, "off")) == "on"
|
||||||
|
}
|
||||||
22
pkg/auth/ldap/const.go
Normal file
22
pkg/auth/ldap/const.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2020 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ldap
|
||||||
|
|
||||||
|
const (
|
||||||
|
// const for ldap configuration
|
||||||
|
MCSLDAPEnabled = "MCS_LDAP_ENABLED"
|
||||||
|
)
|
||||||
@@ -22,13 +22,14 @@ import (
|
|||||||
"strings"
|
"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
@@ -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
|
||||||
|
|||||||
@@ -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()}`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}`}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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 + "")}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
259
portal-ui/src/screens/Console/Watch/Watch.tsx
Normal file
259
portal-ui/src/screens/Console/Watch/Watch.tsx
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2020 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Typography,
|
||||||
|
TextField
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
|
||||||
|
import { AppState } from "../../../store";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { watchMessageReceived, watchResetMessages } from "./actions";
|
||||||
|
import { EventInfo, BucketList, Bucket } from "./types";
|
||||||
|
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||||
|
import { niceBytes, timeFromDate } from "../../../common/utils";
|
||||||
|
import { wsProtocol } from "../../../utils/wsUtils";
|
||||||
|
import api from "../../../common/api";
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
|
||||||
|
const styles = (theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
watchList: {
|
||||||
|
background: "white",
|
||||||
|
maxHeight: "400px",
|
||||||
|
overflow: "auto",
|
||||||
|
"& ul": {
|
||||||
|
margin: "4px",
|
||||||
|
padding: "0px"
|
||||||
|
},
|
||||||
|
"& ul li": {
|
||||||
|
listStyle: "none",
|
||||||
|
margin: "0px",
|
||||||
|
padding: "0px",
|
||||||
|
borderBottom: "1px solid #dedede"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actionsTray: {
|
||||||
|
textAlign: "right",
|
||||||
|
"& button": {
|
||||||
|
marginLeft: 10,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inputField: {
|
||||||
|
background: "#FFFFFF",
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 5,
|
||||||
|
marginLeft: 10,
|
||||||
|
boxShadow: "0px 3px 6px #00000012"
|
||||||
|
},
|
||||||
|
fieldContainer: {
|
||||||
|
background: "#FFFFFF",
|
||||||
|
padding: 0,
|
||||||
|
borderRadius: 5,
|
||||||
|
marginLeft: 10,
|
||||||
|
textAlign: "left",
|
||||||
|
minWidth: "206px",
|
||||||
|
boxShadow: "0px 3px 6px #00000012"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IWatch {
|
||||||
|
classes: any;
|
||||||
|
watchMessageReceived: typeof watchMessageReceived;
|
||||||
|
watchResetMessages: typeof watchResetMessages;
|
||||||
|
messages: EventInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Watch = ({
|
||||||
|
classes,
|
||||||
|
watchMessageReceived,
|
||||||
|
watchResetMessages,
|
||||||
|
messages
|
||||||
|
}: IWatch) => {
|
||||||
|
const [start, setStart] = useState(false);
|
||||||
|
const [bucketName, setBucketName] = useState("Select Bucket");
|
||||||
|
const [prefix, setPrefix] = useState("");
|
||||||
|
const [suffix, setSuffix] = useState("");
|
||||||
|
const [bucketList, setBucketList] = useState<Bucket[]>([]);
|
||||||
|
|
||||||
|
const fetchBucketList = () => {
|
||||||
|
api
|
||||||
|
.invoke("GET", `/api/v1/buckets`)
|
||||||
|
.then((res: BucketList) => {
|
||||||
|
let buckets: Bucket[] = [];
|
||||||
|
if (res.buckets !== null) {
|
||||||
|
buckets = res.buckets;
|
||||||
|
}
|
||||||
|
setBucketList(buckets);
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBucketList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
watchResetMessages();
|
||||||
|
// begin watch if bucketName in bucketList and start pressed
|
||||||
|
if (start && bucketList.some(bucket => bucket.name === bucketName)) {
|
||||||
|
const url = new URL(window.location.toString());
|
||||||
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
|
const port = isDev ? "9090" : url.port;
|
||||||
|
|
||||||
|
const wsProt = wsProtocol(url.protocol);
|
||||||
|
const c = new W3CWebSocket(`${wsProt}://${url.hostname}:${port}/ws/watch/${bucketName}?prefix=${prefix}&suffix=${suffix}`);
|
||||||
|
|
||||||
|
let interval: any | null = null;
|
||||||
|
if (c !== null) {
|
||||||
|
c.onopen = () => {
|
||||||
|
console.log("WebSocket Client Connected");
|
||||||
|
c.send("ok");
|
||||||
|
interval = setInterval(() => {
|
||||||
|
c.send("ok");
|
||||||
|
}, 10 * 1000);
|
||||||
|
};
|
||||||
|
c.onmessage = (message: IMessageEvent) => {
|
||||||
|
let m: EventInfo = JSON.parse(message.data.toString());
|
||||||
|
m.Time = new Date(m.Time.toString());
|
||||||
|
m.key = Math.random();
|
||||||
|
watchMessageReceived(m);
|
||||||
|
};
|
||||||
|
c.onclose = () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
console.log("connection closed by server");
|
||||||
|
};
|
||||||
|
return () => {
|
||||||
|
// close websocket on useEffect cleanup
|
||||||
|
c.close(1000);
|
||||||
|
clearInterval(interval);
|
||||||
|
console.log("closing websockets");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// reset start status
|
||||||
|
setStart(false);
|
||||||
|
}
|
||||||
|
}, [watchMessageReceived, start]);
|
||||||
|
|
||||||
|
const bucketNames = bucketList.map(bucketName => ({
|
||||||
|
label: bucketName.name,
|
||||||
|
value: bucketName.name
|
||||||
|
}));
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography variant="h6">Watch</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<br />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} className={classes.actionsTray}>
|
||||||
|
<FormControl variant="outlined">
|
||||||
|
<Select
|
||||||
|
id="bucket-name"
|
||||||
|
name="bucket-name"
|
||||||
|
value={bucketName}
|
||||||
|
onChange={(e) => { setBucketName(e.target.value as string) }}
|
||||||
|
className={classes.fieldContainer}
|
||||||
|
disabled={start}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
value={bucketName}
|
||||||
|
key={`select-bucket-name-default`}
|
||||||
|
disabled={true}
|
||||||
|
>
|
||||||
|
Select Bucket
|
||||||
|
</MenuItem>
|
||||||
|
{bucketNames.map(option => (
|
||||||
|
<MenuItem
|
||||||
|
value={option.value}
|
||||||
|
key={`select-bucket-name-${option.label}`}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<TextField
|
||||||
|
placeholder="Prefix"
|
||||||
|
className={classes.inputField}
|
||||||
|
id="prefix-resource"
|
||||||
|
label=""
|
||||||
|
disabled={start}
|
||||||
|
InputProps={{
|
||||||
|
disableUnderline: true,
|
||||||
|
}}
|
||||||
|
onChange={(e) => { setPrefix(e.target.value) }}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
placeholder="Suffix"
|
||||||
|
className={classes.inputField}
|
||||||
|
id="suffix-resource"
|
||||||
|
label=""
|
||||||
|
disabled={start}
|
||||||
|
InputProps={{
|
||||||
|
disableUnderline: true,
|
||||||
|
}}
|
||||||
|
onChange={(e) => { setSuffix(e.target.value) }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={start}
|
||||||
|
onClick={() => setStart(true)}
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<br />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<div className={classes.watchList}>
|
||||||
|
<ul>
|
||||||
|
{messages.map(m => {
|
||||||
|
return (
|
||||||
|
<li key={m.key}>
|
||||||
|
{timeFromDate(m.Time)} - {niceBytes(m.Size + "")} - {m.Type} - {m.Path}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapState = (state: AppState) => ({
|
||||||
|
messages: state.watch.messages
|
||||||
|
});
|
||||||
|
|
||||||
|
const connector = connect(mapState, {
|
||||||
|
watchMessageReceived: watchMessageReceived,
|
||||||
|
watchResetMessages: watchResetMessages
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connector(withStyles(styles)(Watch));
|
||||||
47
portal-ui/src/screens/Console/Watch/actions.ts
Normal file
47
portal-ui/src/screens/Console/Watch/actions.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2020 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { EventInfo } from "./types";
|
||||||
|
|
||||||
|
export const WATCH_MESSAGE_RECEIVED = "WATCH_MESSAGE_RECEIVED";
|
||||||
|
export const WATCH_RESET_MESSAGES = "WATCH_RESET_MESSAGES";
|
||||||
|
|
||||||
|
interface WatchMessageReceivedAction {
|
||||||
|
type: typeof WATCH_MESSAGE_RECEIVED;
|
||||||
|
message: EventInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WatchResetMessagesAction {
|
||||||
|
type: typeof WATCH_RESET_MESSAGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type WatchActionTypes =
|
||||||
|
| WatchMessageReceivedAction
|
||||||
|
| WatchResetMessagesAction;
|
||||||
|
|
||||||
|
export function watchMessageReceived(message: EventInfo) {
|
||||||
|
return {
|
||||||
|
type: WATCH_MESSAGE_RECEIVED,
|
||||||
|
message: message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function watchResetMessages() {
|
||||||
|
return {
|
||||||
|
type: WATCH_RESET_MESSAGES
|
||||||
|
};
|
||||||
|
}
|
||||||
50
portal-ui/src/screens/Console/Watch/reducers.ts
Normal file
50
portal-ui/src/screens/Console/Watch/reducers.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2020 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import {
|
||||||
|
WATCH_MESSAGE_RECEIVED,
|
||||||
|
WATCH_RESET_MESSAGES,
|
||||||
|
WatchActionTypes
|
||||||
|
} from "./actions";
|
||||||
|
import { EventInfo } from "./types";
|
||||||
|
|
||||||
|
export interface WatchState {
|
||||||
|
messages: EventInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: WatchState = {
|
||||||
|
messages: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export function watchReducer(
|
||||||
|
state = initialState,
|
||||||
|
action: WatchActionTypes
|
||||||
|
): WatchState {
|
||||||
|
switch (action.type) {
|
||||||
|
case WATCH_MESSAGE_RECEIVED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
messages: [...state.messages, action.message]
|
||||||
|
};
|
||||||
|
case WATCH_RESET_MESSAGES:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
messages: []
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
portal-ui/src/screens/Console/Watch/types.ts
Normal file
36
portal-ui/src/screens/Console/Watch/types.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2020 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
export interface EventInfo {
|
||||||
|
Time: Date;
|
||||||
|
Size: number;
|
||||||
|
UserMetadata: Map<string, string>;
|
||||||
|
Path: string;
|
||||||
|
Type: string;
|
||||||
|
Host: string;
|
||||||
|
Port: string;
|
||||||
|
UserAgent: string;
|
||||||
|
key: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Bucket {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BucketList {
|
||||||
|
buckets: Bucket[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
@@ -19,11 +19,13 @@ import thunk from "redux-thunk";
|
|||||||
import { systemReducer } from "./reducer";
|
import { 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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
95
restapi/tls.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
// This file is part of MinIO Orchestrator
|
||||||
|
// Copyright (c) 2020 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package restapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
certDontExists = "File certificate doesn't exists: %s"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareSTSClientTransport() *http.Transport {
|
||||||
|
// This takes github.com/minio/minio/pkg/madmin/transport.go as an example
|
||||||
|
//
|
||||||
|
// DefaultTransport - this default transport is similar to
|
||||||
|
// http.DefaultTransport but with additional param DisableCompression
|
||||||
|
// is set to true to avoid decompressing content with 'gzip' encoding.
|
||||||
|
DefaultTransport := &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
KeepAlive: 15 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
MaxIdleConns: 1024,
|
||||||
|
MaxIdleConnsPerHost: 1024,
|
||||||
|
ResponseHeaderTimeout: 60 * time.Second,
|
||||||
|
IdleConnTimeout: 60 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
DisableCompression: true,
|
||||||
|
}
|
||||||
|
// If Minio instance is running with TLS enabled and it's using a self-signed certificate
|
||||||
|
// or a certificate issued by a custom certificate authority we prepare a new custom *http.Transport
|
||||||
|
if getMinIOEndpointIsSecure() {
|
||||||
|
caCertFileNames := getMinioServerTLSRootCAs()
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
// Can't use SSLv3 because of POODLE and BEAST
|
||||||
|
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
|
||||||
|
// Can't use TLSv1.1 because of RC4 cipher usage
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
// If root CAs are configured we save them to the http.Client RootCAs store
|
||||||
|
if len(caCertFileNames) > 0 {
|
||||||
|
certs := x509.NewCertPool()
|
||||||
|
for _, caCert := range caCertFileNames {
|
||||||
|
// Validate certificate exists
|
||||||
|
if FileExists(caCert) {
|
||||||
|
pemData, err := ioutil.ReadFile(caCert)
|
||||||
|
if err != nil {
|
||||||
|
// if there was an error reading pem file stop mcs
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
certs.AppendCertsFromPEM(pemData)
|
||||||
|
} else {
|
||||||
|
// if provided cert filename doesn't exists stop mcs
|
||||||
|
panic(fmt.Sprintf(certDontExists, caCert))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tlsConfig.RootCAs = certs
|
||||||
|
}
|
||||||
|
DefaultTransport.TLSClientConfig = tlsConfig
|
||||||
|
}
|
||||||
|
return DefaultTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareSTSClient returns an http.Client with custom configurations need it by *credentials.STSAssumeRole
|
||||||
|
// custom configurations include skipVerification flag, and root CA certificates
|
||||||
|
func PrepareSTSClient() *http.Client {
|
||||||
|
transport := prepareSTSClientTransport()
|
||||||
|
// Return http client with default configuration
|
||||||
|
return &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,14 +40,16 @@ func registerBucketEventsHandlers(api *operations.McsAPI) {
|
|||||||
})
|
})
|
||||||
// create bucket event
|
// 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
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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
165
restapi/user_watch.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2020 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package restapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
mc "github.com/minio/mc/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
type watchOptions struct {
|
||||||
|
BucketName string
|
||||||
|
mc.WatchOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// startWatch starts by setting a websocket reader that
|
||||||
|
// will check for a heartbeat.
|
||||||
|
//
|
||||||
|
// A WaitGroup is used to handle goroutines and to ensure
|
||||||
|
// all finish in the proper order. If any, sendWatchInfo()
|
||||||
|
// or wsReadCheck() returns, watch should end.
|
||||||
|
func startWatch(conn WSConn, client MCS3Client, options watchOptions) (mError error) {
|
||||||
|
// a WaitGroup waits for a collection of goroutines to finish
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
// a cancel context is needed to end all goroutines used
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Set number of goroutines to wait. wg.Wait()
|
||||||
|
// waits until counter is zero (all are done)
|
||||||
|
wg.Add(3)
|
||||||
|
// start go routine for reading websocket heartbeat
|
||||||
|
readErr := wsReadCheck(ctx, &wg, conn)
|
||||||
|
// send Stream of watch events to the ws c.connection
|
||||||
|
ch := sendWatchInfo(ctx, &wg, conn, client, options)
|
||||||
|
// If wsReadCheck returns it means that it is not possible to check
|
||||||
|
// ws heartbeat anymore so we stop from doing Watch, cancel context
|
||||||
|
// for all goroutines.
|
||||||
|
go func(wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := <-readErr; err != nil {
|
||||||
|
log.Println("error on wsReadCheck:", err)
|
||||||
|
mError = err
|
||||||
|
}
|
||||||
|
// cancel context for all goroutines.
|
||||||
|
cancel()
|
||||||
|
}(&wg)
|
||||||
|
|
||||||
|
if err := <-ch; err != nil {
|
||||||
|
mError = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if ch closes for any reason,
|
||||||
|
// cancel context for all goroutines
|
||||||
|
cancel()
|
||||||
|
// wait all goroutines to finish
|
||||||
|
wg.Wait()
|
||||||
|
return mError
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendWatchInfo sends stream of Watch Event to the ws connection
|
||||||
|
func sendWatchInfo(ctx context.Context, wg *sync.WaitGroup, conn WSConn, wsc MCS3Client, options watchOptions) <-chan error {
|
||||||
|
// decrements the WaitGroup counter
|
||||||
|
// by one when the function returns
|
||||||
|
defer wg.Done()
|
||||||
|
ch := make(chan error)
|
||||||
|
go func(ch chan<- error) {
|
||||||
|
defer close(ch)
|
||||||
|
wo, pErr := wsc.watch(options.WatchOptions)
|
||||||
|
if pErr != nil {
|
||||||
|
fmt.Println("error initializing watch:", pErr.Cause)
|
||||||
|
ch <- pErr.Cause
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
close(wo.DoneChan)
|
||||||
|
return
|
||||||
|
case events, ok := <-wo.Events():
|
||||||
|
// zero value returned because the channel is closed and empty
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, event := range events {
|
||||||
|
// Serialize message to be sent
|
||||||
|
bytes, err := json.Marshal(event)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("error on json.Marshal:", err)
|
||||||
|
ch <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Send Message through websocket connection
|
||||||
|
err = conn.writeMessage(websocket.TextMessage, bytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error writeMessage:", err)
|
||||||
|
ch <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case pErr, ok := <-wo.Errors():
|
||||||
|
// zero value returned because the channel is closed and empty
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if pErr != nil {
|
||||||
|
log.Println("error on watch:", pErr.Cause)
|
||||||
|
ch <- pErr.Cause
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(ch)
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOptionsFromReq gets bucket name, events, prefix, suffix from a websocket
|
||||||
|
// watch path if defined.
|
||||||
|
// path come as : `/watch/bucket1` and query params come on request form
|
||||||
|
func getOptionsFromReq(req *http.Request) watchOptions {
|
||||||
|
wOptions := watchOptions{}
|
||||||
|
// Default Events if not defined
|
||||||
|
wOptions.Events = []string{"put", "get", "delete"}
|
||||||
|
|
||||||
|
re := regexp.MustCompile(`(/watch/)(.*?$)`)
|
||||||
|
matches := re.FindAllSubmatch([]byte(req.URL.Path), -1)
|
||||||
|
// len matches is always 3
|
||||||
|
// matches comes as e.g.
|
||||||
|
// [["...", "/watch/" "bucket1"]]
|
||||||
|
// [["/watch/" "/watch/" ""]]
|
||||||
|
|
||||||
|
// bucket name is on the second group, third position
|
||||||
|
wOptions.BucketName = strings.TrimSpace(string(matches[0][2]))
|
||||||
|
|
||||||
|
events := req.FormValue("events")
|
||||||
|
if strings.TrimSpace(events) != "" {
|
||||||
|
wOptions.Events = strings.Split(events, ",")
|
||||||
|
}
|
||||||
|
wOptions.Prefix = req.FormValue("prefix")
|
||||||
|
wOptions.Suffix = req.FormValue("suffix")
|
||||||
|
return wOptions
|
||||||
|
}
|
||||||
291
restapi/user_watch_test.go
Normal file
291
restapi/user_watch_test.go
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2020 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package restapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
mc "github.com/minio/mc/cmd"
|
||||||
|
"github.com/minio/mc/pkg/probe"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// assigning mock at runtime instead of compile time
|
||||||
|
var mcWatchMock func(options mc.WatchOptions) (*mc.WatchObject, *probe.Error)
|
||||||
|
|
||||||
|
// implements mc.S3Client.Watch()
|
||||||
|
func (c s3ClientMock) watch(options mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
|
||||||
|
return mcWatchMock(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatch(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
client := s3ClientMock{}
|
||||||
|
mockWSConn := mockConn{}
|
||||||
|
|
||||||
|
function := "startWatch()"
|
||||||
|
|
||||||
|
testStreamSize := 5
|
||||||
|
testReceiver := make(chan []mc.EventInfo, testStreamSize)
|
||||||
|
textToReceive := "test message"
|
||||||
|
testOptions := watchOptions{}
|
||||||
|
testOptions.BucketName = "bucktest"
|
||||||
|
testOptions.Prefix = "file/"
|
||||||
|
testOptions.Suffix = ".png"
|
||||||
|
|
||||||
|
// Test-1: Serve Watch with no errors until Watch finishes sending
|
||||||
|
// define mock function behavior
|
||||||
|
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
|
||||||
|
wo := &mc.WatchObject{
|
||||||
|
EventInfoChan: make(chan []mc.EventInfo),
|
||||||
|
ErrorChan: make(chan *probe.Error),
|
||||||
|
DoneChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
// Only success, start a routine to start reading line by line.
|
||||||
|
go func(wo *mc.WatchObject) {
|
||||||
|
defer wo.Close()
|
||||||
|
lines := make([]int, testStreamSize)
|
||||||
|
// mocking sending 5 lines of info
|
||||||
|
for range lines {
|
||||||
|
info := []mc.EventInfo{
|
||||||
|
mc.EventInfo{
|
||||||
|
UserAgent: textToReceive,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wo.Events() <- info
|
||||||
|
}
|
||||||
|
}(wo)
|
||||||
|
return wo, nil
|
||||||
|
}
|
||||||
|
// mock function of conn.ReadMessage(), no error on read
|
||||||
|
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||||
|
return 0, []byte{}, nil
|
||||||
|
}
|
||||||
|
writesCount := 1
|
||||||
|
// mock connection WriteMessage() no error
|
||||||
|
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||||
|
// emulate that receiver gets the message written
|
||||||
|
var t []mc.EventInfo
|
||||||
|
_ = json.Unmarshal(data, &t)
|
||||||
|
if writesCount == testStreamSize {
|
||||||
|
// for testing we need to close the receiver channel
|
||||||
|
close(testReceiver)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
testReceiver <- t
|
||||||
|
writesCount++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := startWatch(mockWSConn, client, testOptions); err != nil {
|
||||||
|
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||||
|
}
|
||||||
|
// check that the TestReceiver got the same number of data from Console.
|
||||||
|
for i := range testReceiver {
|
||||||
|
for _, val := range i {
|
||||||
|
assert.Equal(textToReceive, val.UserAgent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test-2: if error happens while writing, return error
|
||||||
|
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||||
|
return fmt.Errorf("error on write")
|
||||||
|
}
|
||||||
|
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
|
||||||
|
assert.Equal("error on write", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test-3: error happens while reading, unexpected Close Error should return error.
|
||||||
|
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// mock function of conn.ReadMessage(), returns unexpected Close Error CloseAbnormalClosure
|
||||||
|
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||||
|
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""}
|
||||||
|
}
|
||||||
|
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
|
||||||
|
assert.Equal("websocket: close 1006 (abnormal closure)", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test-4: error happens while reading, expected Close Error NormalClosure
|
||||||
|
// expected Close Error should not return an error, just end Console.
|
||||||
|
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||||
|
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""}
|
||||||
|
}
|
||||||
|
if err := startWatch(mockWSConn, client, testOptions); err != nil {
|
||||||
|
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test-5: error happens while reading, expected Close Error CloseGoingAway
|
||||||
|
// expected Close Error should not return an error, just return.
|
||||||
|
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||||
|
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""}
|
||||||
|
}
|
||||||
|
if err := startWatch(mockWSConn, client, testOptions); err != nil {
|
||||||
|
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test-6: error happens while reading, non Close Error Type should be returned as
|
||||||
|
// error
|
||||||
|
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||||
|
return 0, []byte{}, fmt.Errorf("error on read")
|
||||||
|
}
|
||||||
|
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
|
||||||
|
assert.Equal("error on read", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test-7: error happens on Watch, watch should stop
|
||||||
|
// and error shall be returned.
|
||||||
|
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
|
||||||
|
wo := &mc.WatchObject{
|
||||||
|
EventInfoChan: make(chan []mc.EventInfo),
|
||||||
|
ErrorChan: make(chan *probe.Error),
|
||||||
|
DoneChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
// Only success, start a routine to start reading line by line.
|
||||||
|
go func(wo *mc.WatchObject) {
|
||||||
|
defer wo.Close()
|
||||||
|
lines := make([]int, testStreamSize)
|
||||||
|
// mocking sending 5 lines of info
|
||||||
|
for range lines {
|
||||||
|
info := []mc.EventInfo{
|
||||||
|
mc.EventInfo{
|
||||||
|
UserAgent: textToReceive,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wo.Events() <- info
|
||||||
|
}
|
||||||
|
wo.Errors() <- &probe.Error{Cause: fmt.Errorf("error on Watch")}
|
||||||
|
}(wo)
|
||||||
|
return wo, nil
|
||||||
|
}
|
||||||
|
// mock function of conn.ReadMessage(), no error on read, should stay unless
|
||||||
|
// context is done.
|
||||||
|
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||||
|
return 0, []byte{}, nil
|
||||||
|
}
|
||||||
|
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
|
||||||
|
assert.Equal("error on Watch", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test-8: error happens on Watch, watch should stop
|
||||||
|
// and error shall be returned.
|
||||||
|
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
|
||||||
|
return nil, &probe.Error{Cause: fmt.Errorf("error on Watch")}
|
||||||
|
}
|
||||||
|
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
|
||||||
|
assert.Equal("error on Watch", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test-9: return nil on error on Watch
|
||||||
|
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
|
||||||
|
wo := &mc.WatchObject{
|
||||||
|
EventInfoChan: make(chan []mc.EventInfo),
|
||||||
|
ErrorChan: make(chan *probe.Error),
|
||||||
|
DoneChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
// Only success, start a routine to start reading line by line.
|
||||||
|
go func(wo *mc.WatchObject) {
|
||||||
|
defer wo.Close()
|
||||||
|
lines := make([]int, testStreamSize)
|
||||||
|
// mocking sending 5 lines of info
|
||||||
|
for range lines {
|
||||||
|
info := []mc.EventInfo{
|
||||||
|
mc.EventInfo{
|
||||||
|
UserAgent: textToReceive,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wo.Events() <- info
|
||||||
|
}
|
||||||
|
wo.Events() <- nil
|
||||||
|
wo.Errors() <- nil
|
||||||
|
}(wo)
|
||||||
|
return wo, nil
|
||||||
|
}
|
||||||
|
if err := startWatch(mockWSConn, client, testOptions); err != nil {
|
||||||
|
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||||
|
}
|
||||||
|
// check that the TestReceiver got the same number of data from Console.
|
||||||
|
for i := range testReceiver {
|
||||||
|
for _, val := range i {
|
||||||
|
assert.Equal(textToReceive, val.UserAgent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test-9: getOptionsFromReq return parameters from path
|
||||||
|
u, err := url.Parse("http://localhost/api/v1/watch/bucket1?prefix=&suffix=.jpg&events=put,get")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
|
||||||
|
}
|
||||||
|
req := &http.Request{
|
||||||
|
URL: u,
|
||||||
|
}
|
||||||
|
opts := getOptionsFromReq(req)
|
||||||
|
expectedOptions := watchOptions{
|
||||||
|
BucketName: "bucket1",
|
||||||
|
}
|
||||||
|
expectedOptions.Prefix = ""
|
||||||
|
expectedOptions.Suffix = ".jpg"
|
||||||
|
expectedOptions.Events = []string{"put", "get"}
|
||||||
|
assert.Equal(expectedOptions.BucketName, opts.BucketName)
|
||||||
|
assert.Equal(expectedOptions.Prefix, opts.Prefix)
|
||||||
|
assert.Equal(expectedOptions.Suffix, opts.Suffix)
|
||||||
|
assert.Equal(expectedOptions.Events, opts.Events)
|
||||||
|
|
||||||
|
// Test-9: getOptionsFromReq return default events if not defined
|
||||||
|
u, err = url.Parse("http://localhost/api/v1/watch/bucket1?prefix=&suffix=.jpg&events=")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
|
||||||
|
}
|
||||||
|
req = &http.Request{
|
||||||
|
URL: u,
|
||||||
|
}
|
||||||
|
opts = getOptionsFromReq(req)
|
||||||
|
expectedOptions = watchOptions{
|
||||||
|
BucketName: "bucket1",
|
||||||
|
}
|
||||||
|
expectedOptions.Prefix = ""
|
||||||
|
expectedOptions.Suffix = ".jpg"
|
||||||
|
expectedOptions.Events = []string{"put", "get", "delete"}
|
||||||
|
assert.Equal(expectedOptions.BucketName, opts.BucketName)
|
||||||
|
assert.Equal(expectedOptions.Prefix, opts.Prefix)
|
||||||
|
assert.Equal(expectedOptions.Suffix, opts.Suffix)
|
||||||
|
assert.Equal(expectedOptions.Events, opts.Events)
|
||||||
|
|
||||||
|
// Test-10: getOptionsFromReq return default events if not defined
|
||||||
|
u, err = url.Parse("http://localhost/api/v1/watch/bucket2?prefix=&suffix=")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
|
||||||
|
}
|
||||||
|
req = &http.Request{
|
||||||
|
URL: u,
|
||||||
|
}
|
||||||
|
opts = getOptionsFromReq(req)
|
||||||
|
expectedOptions = watchOptions{
|
||||||
|
BucketName: "bucket2",
|
||||||
|
}
|
||||||
|
expectedOptions.Events = []string{"put", "get", "delete"}
|
||||||
|
assert.Equal(expectedOptions.BucketName, opts.BucketName)
|
||||||
|
assert.Equal(expectedOptions.Prefix, opts.Prefix)
|
||||||
|
assert.Equal(expectedOptions.Suffix, opts.Suffix)
|
||||||
|
assert.Equal(expectedOptions.Events, opts.Events)
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package restapi
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user