Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c49a7bbe3c | ||
|
|
3bb317535c | ||
|
|
989e6f3471 | ||
|
|
35d575e7ac | ||
|
|
92a8aab07d | ||
|
|
c5b2419191 | ||
|
|
732e0ef683 | ||
|
|
e8491d80cb | ||
|
|
35c3b53a23 | ||
|
|
6fef30f29d | ||
|
|
acf480fd25 | ||
|
|
8bbc4f0192 | ||
|
|
48e7991f11 | ||
|
|
5e9b0652b0 | ||
|
|
438211199d | ||
|
|
0f77a32656 | ||
|
|
9f3e99ede8 | ||
|
|
a8c07c0969 | ||
|
|
cf8472b04c | ||
|
|
317a7ebbd3 | ||
|
|
63f4150232 | ||
|
|
3bfc2556fc | ||
|
|
511cc47d2b | ||
|
|
9660650f41 | ||
|
|
9ac754d4de | ||
|
|
646318e1f6 | ||
|
|
beb1ac7d04 | ||
|
|
f3a9311374 |
10
.github/workflows/go.yml
vendored
10
.github/workflows/go.yml
vendored
@@ -18,21 +18,19 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: on
|
||||
GOOS: linux
|
||||
run: |
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0
|
||||
$(go env GOPATH)/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
|
||||
go mod vendor
|
||||
go test -v -race ./...
|
||||
make verifiers
|
||||
make test
|
||||
make mcs
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
linters-settings:
|
||||
golint:
|
||||
min-confidence: 0
|
||||
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
@@ -14,4 +17,8 @@ linters:
|
||||
- gosimple
|
||||
- deadcode
|
||||
- unparam
|
||||
- unused
|
||||
- structcheck
|
||||
|
||||
service:
|
||||
golangci-lint-version: 1.21.0 # use the fixed version to not introduce new linters unexpectedly
|
||||
|
||||
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
|
||||
```
|
||||
24
Makefile
24
Makefile
@@ -6,7 +6,23 @@ default: mcs
|
||||
.PHONY: mcs
|
||||
mcs:
|
||||
@echo "Building mcs binary to './mcs'"
|
||||
@(CGO_ENABLED=0 go build -trimpath --tags=kqueue --ldflags "-s -w" -o mcs ./cmd/mcs)
|
||||
@(GO111MODULE=on CGO_ENABLED=0 go build -trimpath --tags=kqueue --ldflags "-s -w" -o mcs ./cmd/mcs)
|
||||
|
||||
getdeps:
|
||||
@mkdir -p ${GOPATH}/bin
|
||||
@which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.27.0)
|
||||
|
||||
verifiers: getdeps fmt lint
|
||||
|
||||
fmt:
|
||||
@echo "Running $@ check"
|
||||
@GO111MODULE=on gofmt -d cmd/
|
||||
@GO111MODULE=on gofmt -d pkg/
|
||||
|
||||
lint:
|
||||
@echo "Running $@ check"
|
||||
@GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
|
||||
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
|
||||
|
||||
install: mcs
|
||||
@echo "Installing mcs binary to '$(GOPATH)/bin/mcs'"
|
||||
@@ -21,11 +37,11 @@ assets:
|
||||
@(cd portal-ui; yarn install; make build-static; cd ..)
|
||||
|
||||
test:
|
||||
@(go test -race -v github.com/minio/mcs/restapi/...)
|
||||
@(go test -race -v github.com/minio/mcs/pkg/auth/...)
|
||||
@(GO111MODULE=on go test -race -v github.com/minio/mcs/restapi/...)
|
||||
@(GO111MODULE=on go test -race -v github.com/minio/mcs/pkg/...)
|
||||
|
||||
coverage:
|
||||
@(go test -v -coverprofile=coverage.out github.com/minio/mcs/restapi/... && go tool cover -html=coverage.out && open coverage.html)
|
||||
@(GO111MODULE=on go test -v -coverprofile=coverage.out github.com/minio/mcs/restapi/... && go tool cover -html=coverage.out && open coverage.html)
|
||||
|
||||
clean:
|
||||
@echo "Cleaning up all the generated files"
|
||||
|
||||
93
README.md
93
README.md
@@ -14,31 +14,30 @@ $ mc admin user add myminio mcs YOURMCSSECRET
|
||||
$ set -o history
|
||||
```
|
||||
|
||||
2. Create a policy for `mcs`
|
||||
2. Create a policy for `mcs` with access to everything (for testing and debugging)
|
||||
|
||||
```
|
||||
$ cat > mcsAdmin.json << EOF
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"admin:*"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Sid": ""
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:*"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
],
|
||||
"Sid": ""
|
||||
}
|
||||
]
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Action": [
|
||||
"admin:*"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Sid": ""
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:*"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
],
|
||||
"Sid": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
$ mc admin policy add myminio mcsAdmin mcsAdmin.json
|
||||
@@ -50,6 +49,49 @@ $ mc admin policy add myminio mcsAdmin mcsAdmin.json
|
||||
$ mc admin policy set myminio mcsAdmin user=mcs
|
||||
```
|
||||
|
||||
|
||||
### Note
|
||||
Additionally, you can create policies to limit the privileges for `mcs` users, for example, if you want the user to only have access to dashboard, buckets, notifications and watch page, the policy should look like this:
|
||||
```
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Action": [
|
||||
"admin:ServerInfo",
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Sid": ""
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:ListenBucketNotification",
|
||||
"s3:PutBucketNotification",
|
||||
"s3:GetBucketNotification",
|
||||
"s3:ListMultipartUploadParts",
|
||||
"s3:ListBucketMultipartUploads",
|
||||
"s3:ListBucket",
|
||||
"s3:HeadBucket",
|
||||
"s3:GetObject",
|
||||
"s3:GetBucketLocation",
|
||||
"s3:AbortMultipartUpload",
|
||||
"s3:CreateBucket",
|
||||
"s3:PutObject",
|
||||
"s3:DeleteObject",
|
||||
"s3:DeleteBucket",
|
||||
"s3:PutBucketPolicy",
|
||||
"s3:DeleteBucketPolicy",
|
||||
"s3:GetBucketPolicy"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
],
|
||||
"Sid": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Run MCS server
|
||||
To run the server:
|
||||
|
||||
@@ -68,6 +110,15 @@ export MCS_MINIO_SERVER=http://localhost:9000
|
||||
./mcs server
|
||||
```
|
||||
|
||||
## Connect MCS to a Minio using TLS and a self-signed certificate
|
||||
|
||||
```
|
||||
...
|
||||
export MCS_MINIO_SERVER_TLS_SKIP_VERIFICATION=on
|
||||
export MCS_MINIO_SERVER=https://localhost:9000
|
||||
./mcs server
|
||||
```
|
||||
|
||||
You can verify that the apis work by doing the request on `localhost:9090/api/v1/...`
|
||||
|
||||
# Contribute to mcs Project
|
||||
|
||||
6
go.mod
6
go.mod
@@ -17,9 +17,9 @@ require (
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/json-iterator/go v1.1.9
|
||||
github.com/minio/cli v1.22.0
|
||||
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c
|
||||
github.com/minio/minio v0.0.0-20200428222040-c3c3e9087bc1
|
||||
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22
|
||||
github.com/minio/mc v0.0.0-20200515235434-3b479cf92ed6
|
||||
github.com/minio/minio v0.0.0-20200516011754-9cac385aecdb
|
||||
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/stretchr/testify v1.5.1
|
||||
|
||||
57
go.sum
57
go.sum
@@ -12,6 +12,7 @@ github.com/Azure/azure-storage-blob-go v0.8.0 h1:53qhf0Oxa0nOjgbDeeYPUeyiNmafAFE
|
||||
github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
|
||||
github.com/Azure/go-autorest v11.7.1+incompatible h1:M2YZIajBBVekV86x0rr1443Lc1F/Ylxb9w+5EtSyX3Q=
|
||||
github.com/Azure/go-autorest v11.7.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
@@ -30,8 +31,6 @@ github.com/alecthomas/participle v0.2.1 h1:4AVLj1viSGa4LG5HDXKXrm5xRx19SB/rS/skP
|
||||
github.com/alecthomas/participle v0.2.1/go.mod h1:SW6HZGeZgSIpcUWX3fXpfZhuaWHnmoD5KCVaqSaNTkk=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5 h1:nWDRPCyCltiTsANwC/n3QZH7Vww33Npq9MKqlwRzI/c=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM=
|
||||
@@ -42,8 +41,6 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/aws/aws-sdk-go v1.20.21/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
|
||||
github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2 h1:M+TYzBcNIRyzPRg66ndEqUMd7oWDmhvdQmaPC6EZNwM=
|
||||
github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2/go.mod h1:RDu/qcrnpEdJC/p8tx34+YBFqqX71lB7dOX9QE+ZC4M=
|
||||
github.com/beevik/ntp v0.2.0 h1:sGsd+kAXzT0bfVfzJfce04g+dSRfrs+tbQW8lweuYgw=
|
||||
@@ -116,6 +113,7 @@ github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-ldap/ldap v3.0.2+incompatible h1:kD5HQcAzlQ7yrhfn+h+MSABeAy/jAJhvIJ/QDllP44g=
|
||||
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
@@ -241,6 +239,7 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -340,12 +339,16 @@ github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eT
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/cpuid v1.2.2 h1:1xAgYebNnsb9LKCdLOvFWtAxGU/33mjJtyOVbmUa0Us=
|
||||
github.com/klauspost/cpuid v1.2.2/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid v1.2.4 h1:EBfaK0SWSwk+fgk6efYFWdzl8MwRWoOO1gkmiaTXPW4=
|
||||
github.com/klauspost/cpuid v1.2.4/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
|
||||
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/klauspost/readahead v1.3.1 h1:QqXNYvm+VvqYcbrRT4LojUciM0XrznFRIDrbHiJtu/0=
|
||||
github.com/klauspost/readahead v1.3.1/go.mod h1:AH9juHzNH7xqdqFHrMRSHeH2Ps+vFf+kblDqzPFiLJg=
|
||||
github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY=
|
||||
github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
|
||||
github.com/klauspost/reedsolomon v1.9.7 h1:+azeqnT4iNG9UEcWC+7utJ4xXQ9S8pSlkZor0DOArEQ=
|
||||
github.com/klauspost/reedsolomon v1.9.7/go.mod h1:+8WD025Xpby8/kG5h/HDPIFhiiuGEtZOKw+5Y4drAD8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -401,16 +404,16 @@ github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2
|
||||
github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc=
|
||||
github.com/minio/lsync v1.0.1 h1:AVvILxA976xc27hstd1oR+X9PQG0sPSom1MNb1ImfUs=
|
||||
github.com/minio/lsync v1.0.1/go.mod h1:tCFzfo0dlvdGl70IT4IAK/5Wtgb0/BrTmo/jE8pArKA=
|
||||
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c h1:JLr0fYpCleodj9nGB5hfsJU2zPdnNQKqa2bYsIvPhVw=
|
||||
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c/go.mod h1:l9PuOY62zT7AQJqopDjfo/T22AIBJSb2yXPVZf4RlhM=
|
||||
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab h1:9hlqghJl3e3HorXa6ADWsz6ECq790t4iQs07VD9JctM=
|
||||
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab/go.mod h1:v8oQPMMaTkjDwp5cOz1WCElA4Ik+X+0y4On+VMk0fis=
|
||||
github.com/minio/minio v0.0.0-20200428222040-c3c3e9087bc1 h1:DQjH/653WCerOeZCp3BxAgkmRiQybHYiprbTFs+brgA=
|
||||
github.com/minio/minio v0.0.0-20200428222040-c3c3e9087bc1/go.mod h1:HxnN5FYGIii8ZH6d+LH5UNOSSIonbJkYPqP6gWelVO0=
|
||||
github.com/minio/mc v0.0.0-20200515235434-3b479cf92ed6 h1:2SrKe2vLDLwvnYkYrJelrzyGW8t/8HCbr9yDsw+8XSI=
|
||||
github.com/minio/mc v0.0.0-20200515235434-3b479cf92ed6/go.mod h1:U3Jgk0bcSjn+QPUMisrS6nxCWOoQ6rYWSvLCB30apuU=
|
||||
github.com/minio/minio v0.0.0-20200421050159-282c9f790a03/go.mod h1:zBua5AiljGs1Irdl2XEyiJjvZVCVDIG8gjozzRBcVlw=
|
||||
github.com/minio/minio v0.0.0-20200516011754-9cac385aecdb h1:CQC7D3UDnUycuxhwImcVhMSLet/RbShosAnYcvMtEB8=
|
||||
github.com/minio/minio v0.0.0-20200516011754-9cac385aecdb/go.mod h1:wymaytM/HELuwdz7BGZHmQ3XKq2SxPsLeGxyOCaCLiA=
|
||||
github.com/minio/minio-go/v6 v6.0.53 h1:8jzpwiOzZ5Iz7/goFWqNZRICbyWYShbb5rARjrnSCNI=
|
||||
github.com/minio/minio-go/v6 v6.0.53/go.mod h1:DIvC/IApeHX8q1BAMVCXSXwpmrmM+I+iBvhvztQorfI=
|
||||
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22 h1:nZEve4vdUhwHBoV18zRvPDgjL6NYyDJE5QJvz3l9bRs=
|
||||
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
|
||||
github.com/minio/minio-go/v6 v6.0.55-0.20200425081427-89eebdef2af0/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
|
||||
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f h1:ifHrI8+exqLi5RztIWWKS5k+Wu+W7DJisVXwNaCH2zs=
|
||||
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
|
||||
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61 h1:pUSI/WKPdd77gcuoJkSzhJ4wdS8OMDOsOu99MtpXEQA=
|
||||
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61/go.mod h1:4trzEJ7N1nBTd5Tt7OCZT5SEin+WiAXpdJ/WgPkESA8=
|
||||
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
|
||||
@@ -555,6 +558,7 @@ github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94 h1:0ngsPmuP6XIjiFRN
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
@@ -596,6 +600,7 @@ github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
|
||||
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
||||
@@ -608,11 +613,19 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.11.0 h1:gSmpCfs+R47a4yQPAI4xJ0IPDLTRGXskm6UelqNXpqE=
|
||||
go.uber.org/zap v1.11.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
|
||||
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
|
||||
golang.org/x/arch v0.0.0-20190909030613-46d78d1859ac/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -621,11 +634,13 @@ golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f h1:kz4KIr+xcPUsI3VMoqWfPMvtnJ6MGfiVwsWSVzphMO4=
|
||||
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg=
|
||||
@@ -635,6 +650,11 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -665,6 +685,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -711,9 +733,18 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
|
||||
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190914235951-31e00f45c22e h1:nOOVVcLC+/3MeovP40q5lCiWmP1Z1DaN8yn8ngU63hw=
|
||||
golang.org/x/tools v0.0.0-20190914235951-31e00f45c22e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32 h1:Xvf3ZQTm5bjXPxhI7g+dwqsCqadK1rcNtwtszuatetk=
|
||||
golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM=
|
||||
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
@@ -776,4 +807,6 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -27,22 +27,22 @@ import (
|
||||
"github.com/go-openapi/swag"
|
||||
)
|
||||
|
||||
// ServiceAccount service account
|
||||
// ServiceAccountRequest service account request
|
||||
//
|
||||
// swagger:model serviceAccount
|
||||
type ServiceAccount struct {
|
||||
// swagger:model serviceAccountRequest
|
||||
type ServiceAccountRequest struct {
|
||||
|
||||
// policy to be applied to the Service Account if any
|
||||
Policy string `json:"policy,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this service account
|
||||
func (m *ServiceAccount) Validate(formats strfmt.Registry) error {
|
||||
// Validate validates this service account request
|
||||
func (m *ServiceAccountRequest) Validate(formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary interface implementation
|
||||
func (m *ServiceAccount) MarshalBinary() ([]byte, error) {
|
||||
func (m *ServiceAccountRequest) MarshalBinary() ([]byte, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -50,8 +50,8 @@ func (m *ServiceAccount) MarshalBinary() ([]byte, error) {
|
||||
}
|
||||
|
||||
// UnmarshalBinary interface implementation
|
||||
func (m *ServiceAccount) UnmarshalBinary(b []byte) error {
|
||||
var res ServiceAccount
|
||||
func (m *ServiceAccountRequest) UnmarshalBinary(b []byte) error {
|
||||
var res ServiceAccountRequest
|
||||
if err := swag.ReadJSON(b, &res); err != nil {
|
||||
return err
|
||||
}
|
||||
37
models/service_accounts.go
Normal file
37
models/service_accounts.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package models
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"github.com/go-openapi/strfmt"
|
||||
)
|
||||
|
||||
// ServiceAccounts service accounts
|
||||
//
|
||||
// swagger:model serviceAccounts
|
||||
type ServiceAccounts []string
|
||||
|
||||
// Validate validates this service accounts
|
||||
func (m ServiceAccounts) Validate(formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
@@ -36,6 +36,9 @@ import (
|
||||
// swagger:model sessionResponse
|
||||
type SessionResponse struct {
|
||||
|
||||
// pages
|
||||
Pages []string `json:"pages"`
|
||||
|
||||
// status
|
||||
// Enum: [ok]
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
278
pkg/acl/endpoints.go
Normal file
278
pkg/acl/endpoints.go
Normal file
@@ -0,0 +1,278 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package acl
|
||||
|
||||
import iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
|
||||
// endpoints definition
|
||||
var (
|
||||
configuration = "/configurations-list"
|
||||
users = "/users"
|
||||
groups = "/groups"
|
||||
iamPolicies = "/policies"
|
||||
dashboard = "/dashboard"
|
||||
profiling = "/profiling"
|
||||
trace = "/trace"
|
||||
logs = "/logs"
|
||||
watch = "/watch"
|
||||
notifications = "/notification-endpoints"
|
||||
buckets = "/buckets"
|
||||
bucketDetails = "/buckets/:bucketName"
|
||||
serviceAccounts = "/service-accounts"
|
||||
)
|
||||
|
||||
type ConfigurationActionSet struct {
|
||||
actionTypes iampolicy.ActionSet
|
||||
actions iampolicy.ActionSet
|
||||
}
|
||||
|
||||
// configurationActionSet contains the list of admin actions required for this endpoint to work
|
||||
var configurationActionSet = ConfigurationActionSet{
|
||||
actionTypes: iampolicy.NewActionSet(
|
||||
iampolicy.AllAdminActions,
|
||||
),
|
||||
actions: iampolicy.NewActionSet(
|
||||
iampolicy.ConfigUpdateAdminAction,
|
||||
),
|
||||
}
|
||||
|
||||
// logsActionSet contains the list of admin actions required for this endpoint to work
|
||||
var logsActionSet = ConfigurationActionSet{
|
||||
actionTypes: iampolicy.NewActionSet(
|
||||
iampolicy.AllAdminActions,
|
||||
),
|
||||
actions: iampolicy.NewActionSet(
|
||||
iampolicy.ConsoleLogAdminAction,
|
||||
),
|
||||
}
|
||||
|
||||
// dashboardActionSet contains the list of admin actions required for this endpoint to work
|
||||
var dashboardActionSet = ConfigurationActionSet{
|
||||
actionTypes: iampolicy.NewActionSet(
|
||||
iampolicy.AllAdminActions,
|
||||
),
|
||||
actions: iampolicy.NewActionSet(
|
||||
iampolicy.ServerInfoAdminAction,
|
||||
),
|
||||
}
|
||||
|
||||
// groupsActionSet contains the list of admin actions required for this endpoint to work
|
||||
var groupsActionSet = ConfigurationActionSet{
|
||||
actionTypes: iampolicy.NewActionSet(
|
||||
iampolicy.AllAdminActions,
|
||||
),
|
||||
actions: iampolicy.NewActionSet(
|
||||
iampolicy.ListGroupsAdminAction,
|
||||
iampolicy.AddUserToGroupAdminAction,
|
||||
//iampolicy.GetGroupAdminAction,
|
||||
iampolicy.EnableGroupAdminAction,
|
||||
iampolicy.DisableGroupAdminAction,
|
||||
),
|
||||
}
|
||||
|
||||
// iamPoliciesActionSet contains the list of admin actions required for this endpoint to work
|
||||
var iamPoliciesActionSet = ConfigurationActionSet{
|
||||
actionTypes: iampolicy.NewActionSet(
|
||||
iampolicy.AllAdminActions,
|
||||
),
|
||||
actions: iampolicy.NewActionSet(
|
||||
iampolicy.GetPolicyAdminAction,
|
||||
iampolicy.DeletePolicyAdminAction,
|
||||
iampolicy.CreatePolicyAdminAction,
|
||||
iampolicy.AttachPolicyAdminAction,
|
||||
iampolicy.ListUserPoliciesAdminAction,
|
||||
),
|
||||
}
|
||||
|
||||
// profilingActionSet contains the list of admin actions required for this endpoint to work
|
||||
var profilingActionSet = ConfigurationActionSet{
|
||||
actionTypes: iampolicy.NewActionSet(
|
||||
iampolicy.AllAdminActions,
|
||||
),
|
||||
actions: iampolicy.NewActionSet(
|
||||
iampolicy.ProfilingAdminAction,
|
||||
),
|
||||
}
|
||||
|
||||
// traceActionSet contains the list of admin actions required for this endpoint to work
|
||||
var traceActionSet = ConfigurationActionSet{
|
||||
actionTypes: iampolicy.NewActionSet(
|
||||
iampolicy.AllAdminActions,
|
||||
),
|
||||
actions: iampolicy.NewActionSet(
|
||||
iampolicy.TraceAdminAction,
|
||||
),
|
||||
}
|
||||
|
||||
// usersActionSet contains the list of admin actions required for this endpoint to work
|
||||
var usersActionSet = ConfigurationActionSet{
|
||||
actionTypes: iampolicy.NewActionSet(
|
||||
iampolicy.AllAdminActions,
|
||||
),
|
||||
actions: iampolicy.NewActionSet(
|
||||
iampolicy.ListUsersAdminAction,
|
||||
iampolicy.CreateUserAdminAction,
|
||||
iampolicy.DeleteUserAdminAction,
|
||||
iampolicy.GetUserAdminAction,
|
||||
iampolicy.EnableUserAdminAction,
|
||||
iampolicy.DisableUserAdminAction,
|
||||
),
|
||||
}
|
||||
|
||||
// watchActionSet contains the list of admin actions required for this endpoint to work
|
||||
var watchActionSet = ConfigurationActionSet{
|
||||
actionTypes: iampolicy.NewActionSet(
|
||||
iampolicy.AllAdminActions,
|
||||
),
|
||||
actions: iampolicy.NewActionSet(
|
||||
iampolicy.ListenBucketNotificationAction,
|
||||
),
|
||||
}
|
||||
|
||||
// notificationsActionSet contains the list of admin actions required for this endpoint to work
|
||||
var notificationsActionSet = ConfigurationActionSet{
|
||||
actionTypes: iampolicy.NewActionSet(
|
||||
iampolicy.AllActions,
|
||||
),
|
||||
actions: iampolicy.NewActionSet(
|
||||
iampolicy.ListenBucketNotificationAction,
|
||||
iampolicy.PutBucketNotificationAction,
|
||||
iampolicy.GetBucketNotificationAction,
|
||||
),
|
||||
}
|
||||
|
||||
// bucketsActionSet contains the list of admin actions required for this endpoint to work
|
||||
var bucketsActionSet = ConfigurationActionSet{
|
||||
actionTypes: iampolicy.NewActionSet(
|
||||
iampolicy.AllActions,
|
||||
),
|
||||
actions: iampolicy.NewActionSet(
|
||||
// Read access to buckets
|
||||
iampolicy.ListMultipartUploadPartsAction,
|
||||
iampolicy.ListBucketMultipartUploadsAction,
|
||||
iampolicy.ListBucketAction,
|
||||
iampolicy.HeadBucketAction,
|
||||
iampolicy.GetObjectAction,
|
||||
iampolicy.GetBucketLocationAction,
|
||||
// Write access to buckets
|
||||
iampolicy.AbortMultipartUploadAction,
|
||||
iampolicy.CreateBucketAction,
|
||||
iampolicy.PutObjectAction,
|
||||
iampolicy.DeleteObjectAction,
|
||||
iampolicy.DeleteBucketAction,
|
||||
// Assign bucket policies
|
||||
iampolicy.PutBucketPolicyAction,
|
||||
iampolicy.DeleteBucketPolicyAction,
|
||||
iampolicy.GetBucketPolicyAction,
|
||||
),
|
||||
}
|
||||
|
||||
// serviceAccountsActionSet contains the list of admin actions required for this endpoint to work
|
||||
var serviceAccountsActionSet = ConfigurationActionSet{
|
||||
actionTypes: iampolicy.NewActionSet(),
|
||||
actions: iampolicy.NewActionSet(),
|
||||
}
|
||||
|
||||
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
|
||||
var endpointRules = map[string]ConfigurationActionSet{
|
||||
configuration: configurationActionSet,
|
||||
users: usersActionSet,
|
||||
groups: groupsActionSet,
|
||||
iamPolicies: iamPoliciesActionSet,
|
||||
dashboard: dashboardActionSet,
|
||||
profiling: profilingActionSet,
|
||||
trace: traceActionSet,
|
||||
logs: logsActionSet,
|
||||
watch: watchActionSet,
|
||||
notifications: notificationsActionSet,
|
||||
buckets: bucketsActionSet,
|
||||
bucketDetails: bucketsActionSet,
|
||||
serviceAccounts: serviceAccountsActionSet,
|
||||
}
|
||||
|
||||
// GetActionsStringFromPolicy extract the admin/s3 actions from a given policy and return them in []string format
|
||||
//
|
||||
// ie:
|
||||
// {
|
||||
// "Version": "2012-10-17",
|
||||
// "Statement": [{
|
||||
// "Action": [
|
||||
// "admin:ServerInfo",
|
||||
// "admin:CreatePolicy",
|
||||
// "admin:GetUser"
|
||||
// ],
|
||||
// ...
|
||||
// },
|
||||
// {
|
||||
// "Action": [
|
||||
// "s3:ListenBucketNotification",
|
||||
// "s3:PutBucketNotification"
|
||||
// ],
|
||||
// ...
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// Will produce an array like: ["admin:ServerInfo", "admin:CreatePolicy", "admin:GetUser", "s3:ListenBucketNotification", "s3:PutBucketNotification"]\
|
||||
func GetActionsStringFromPolicy(policy *iampolicy.Policy) []string {
|
||||
var actions []string
|
||||
for _, statement := range policy.Statements {
|
||||
// We only care about allowed actions
|
||||
if statement.Effect.IsAllowed(true) {
|
||||
for _, action := range statement.Actions.ToSlice() {
|
||||
actions = append(actions, string(action))
|
||||
}
|
||||
}
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
// actionsStringToActionSet convert a given string array to iampolicy.ActionSet structure
|
||||
// this avoids ending with duplicate actions
|
||||
func actionsStringToActionSet(actions []string) iampolicy.ActionSet {
|
||||
actionsSet := iampolicy.ActionSet{}
|
||||
for _, action := range actions {
|
||||
actionsSet.Add(iampolicy.Action(action))
|
||||
}
|
||||
return actionsSet
|
||||
}
|
||||
|
||||
// GetAuthorizedEndpoints return a list of allowed endpoint based on a provided *iampolicy.Policy
|
||||
// ie: pages the user should have access based on his current privileges
|
||||
func GetAuthorizedEndpoints(actions []string) []string {
|
||||
if len(actions) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
// Prepare new ActionSet structure that will hold all the user actions
|
||||
userAllowedAction := actionsStringToActionSet(actions)
|
||||
allowedEndpoints := []string{}
|
||||
for endpoint, rules := range endpointRules {
|
||||
// check if user policy matches s3:* or admin:* typesIntersection
|
||||
endpointActionTypes := rules.actionTypes
|
||||
typesIntersection := endpointActionTypes.Intersection(userAllowedAction)
|
||||
if len(typesIntersection) == len(endpointActionTypes.ToSlice()) {
|
||||
allowedEndpoints = append(allowedEndpoints, endpoint)
|
||||
continue
|
||||
}
|
||||
// check if user policy matches explicitly defined endpoint required actions
|
||||
endpointRequiredActions := rules.actions
|
||||
actionsIntersection := endpointRequiredActions.Intersection(userAllowedAction)
|
||||
if len(actionsIntersection) == len(endpointRequiredActions.ToSlice()) {
|
||||
allowedEndpoints = append(allowedEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return allowedEndpoints
|
||||
}
|
||||
138
pkg/acl/endpoints_test.go
Normal file
138
pkg/acl/endpoints_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// This file is part of MinIO Orchestrator
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package acl
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
)
|
||||
|
||||
func TestGetAuthorizedEndpoints(t *testing.T) {
|
||||
type args struct {
|
||||
actions []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "dashboard endpoint",
|
||||
args: args{
|
||||
[]string{"admin:ServerInfo"},
|
||||
},
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
name: "policies endpoint",
|
||||
args: args{
|
||||
[]string{
|
||||
"admin:CreatePolicy",
|
||||
"admin:DeletePolicy",
|
||||
"admin:GetPolicy",
|
||||
"admin:AttachUserOrGroupPolicy",
|
||||
"admin:ListUserPolicies",
|
||||
},
|
||||
},
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
name: "all admin endpoints",
|
||||
args: args{
|
||||
[]string{
|
||||
"admin:*",
|
||||
},
|
||||
},
|
||||
want: 10,
|
||||
},
|
||||
{
|
||||
name: "all s3 endpoints",
|
||||
args: args{
|
||||
[]string{
|
||||
"s3:*",
|
||||
},
|
||||
},
|
||||
want: 4,
|
||||
},
|
||||
{
|
||||
name: "all admin and s3 endpoints",
|
||||
args: args{
|
||||
[]string{
|
||||
"admin:*",
|
||||
"s3:*",
|
||||
},
|
||||
},
|
||||
want: 13,
|
||||
},
|
||||
{
|
||||
name: "no endpoints",
|
||||
args: args{
|
||||
[]string{},
|
||||
},
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetAuthorizedEndpoints(tt.args.actions); !reflect.DeepEqual(len(got), tt.want) {
|
||||
t.Errorf("GetAuthorizedEndpoints() = %v, want %v", len(got), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetActionsStringFromPolicy(t *testing.T) {
|
||||
type args struct {
|
||||
policy *iampolicy.Policy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "parse ReadOnly policy",
|
||||
args: args{
|
||||
policy: &iampolicy.ReadOnly,
|
||||
},
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
name: "parse WriteOnly policy",
|
||||
args: args{
|
||||
policy: &iampolicy.WriteOnly,
|
||||
},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "parse AdminDiagnostics policy",
|
||||
args: args{
|
||||
policy: &iampolicy.AdminDiagnostics,
|
||||
},
|
||||
want: 6,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetActionsStringFromPolicy(tt.args.policy); !reflect.DeepEqual(len(got), tt.want) {
|
||||
t.Errorf("GetActionsStringFromPolicy() = %v, want %v", len(got), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
errAuthentication = errors.New("Authentication failed, check your access credentials")
|
||||
errAuthentication = errors.New("authentication failed, check your access credentials")
|
||||
errNoAuthToken = errors.New("JWT token missing")
|
||||
errReadingToken = errors.New("JWT internal data is malformed")
|
||||
errClaimsFormat = errors.New("encrypted jwt claims not in the right format")
|
||||
@@ -57,6 +57,7 @@ type DecryptedClaims struct {
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
SessionToken string
|
||||
Actions []string
|
||||
}
|
||||
|
||||
// JWTAuthenticate takes a jwt, decode it, extract claims and validate the signature
|
||||
@@ -93,9 +94,9 @@ func JWTAuthenticate(token string) (*DecryptedClaims, error) {
|
||||
|
||||
// NewJWTWithClaimsForClient generates a new jwt with claims based on the provided STS credentials, first
|
||||
// encrypts the claims and the sign them
|
||||
func NewJWTWithClaimsForClient(credentials *credentials.Value, audience string) (string, error) {
|
||||
func NewJWTWithClaimsForClient(credentials *credentials.Value, actions []string, audience string) (string, error) {
|
||||
if credentials != nil {
|
||||
encryptedClaims, err := encryptClaims(credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken)
|
||||
encryptedClaims, err := encryptClaims(credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken, actions)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -112,8 +113,8 @@ func NewJWTWithClaimsForClient(credentials *credentials.Value, audience string)
|
||||
|
||||
// encryptClaims() receives the 3 STS claims, concatenate them and encrypt them using AES-GCM
|
||||
// returns a base64 encoded ciphertext
|
||||
func encryptClaims(accessKeyID, secretAccessKey, sessionToken string) (string, error) {
|
||||
payload := []byte(fmt.Sprintf("%s:%s:%s", accessKeyID, secretAccessKey, sessionToken))
|
||||
func encryptClaims(accessKeyID, secretAccessKey, sessionToken string, actions []string) (string, error) {
|
||||
payload := []byte(fmt.Sprintf("%s#%s#%s#%s", accessKeyID, secretAccessKey, sessionToken, strings.Join(actions, ",")))
|
||||
ciphertext, err := encrypt(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -133,16 +134,18 @@ func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
|
||||
log.Println(err)
|
||||
return nil, errClaimsFormat
|
||||
}
|
||||
s := strings.Split(string(plaintext), ":")
|
||||
s := strings.Split(string(plaintext), "#")
|
||||
// Validate that the decrypted string has the right format "accessKeyID:secretAccessKey:sessionToken"
|
||||
if len(s) != 3 {
|
||||
if len(s) != 4 {
|
||||
return nil, errClaimsFormat
|
||||
}
|
||||
accessKeyID, secretAccessKey, sessionToken := s[0], s[1], s[2]
|
||||
accessKeyID, secretAccessKey, sessionToken, actions := s[0], s[1], s[2], s[3]
|
||||
actionsList := strings.Split(actions, ",")
|
||||
return &DecryptedClaims{
|
||||
AccessKeyID: accessKeyID,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
SessionToken: sessionToken,
|
||||
Actions: actionsList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -37,14 +37,14 @@ func TestNewJWTWithClaimsForClient(t *testing.T) {
|
||||
funcAssert := assert.New(t)
|
||||
// Test-1 : NewJWTWithClaimsForClient() is generated correctly without errors
|
||||
function := "NewJWTWithClaimsForClient()"
|
||||
jwt, err := NewJWTWithClaimsForClient(creds, audience)
|
||||
jwt, err := NewJWTWithClaimsForClient(creds, []string{""}, audience)
|
||||
if err != nil || jwt == "" {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err)
|
||||
}
|
||||
// saving jwt for future tests
|
||||
goodToken = jwt
|
||||
// Test-2 : NewJWTWithClaimsForClient() throws error because of empty credentials
|
||||
if _, err = NewJWTWithClaimsForClient(nil, audience); err != nil {
|
||||
if _, err = NewJWTWithClaimsForClient(nil, []string{""}, audience); err != nil {
|
||||
funcAssert.Equal("provided credentials are empty", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func TestJWTAuthenticate(t *testing.T) {
|
||||
}
|
||||
// Test-2 : JWTAuthenticate() return an error because of a tampered jwt
|
||||
if _, err := JWTAuthenticate(badToken); err != nil {
|
||||
funcAssert.Equal("Authentication failed, check your access credentials", err.Error())
|
||||
funcAssert.Equal("authentication failed, check your access credentials", err.Error())
|
||||
}
|
||||
// Test-3 : JWTAuthenticate() return an error because of an empty jwt
|
||||
if _, err := JWTAuthenticate(""); err != nil {
|
||||
|
||||
39
pkg/auth/ldap.go
Normal file
39
pkg/auth/ldap.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/credentials"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidCredentials = errors.New("invalid Credentials")
|
||||
)
|
||||
|
||||
// GetMcsCredentialsFromLDAP authenticates the user against MinIO when the LDAP integration is enabled
|
||||
// if the authentication succeed *credentials.Credentials object is returned and we continue with the normal STSAssumeRole flow
|
||||
func GetMcsCredentialsFromLDAP(endpoint, ldapUser, ldapPassword string) (*credentials.Credentials, error) {
|
||||
creds, err := credentials.NewLDAPIdentity(endpoint, ldapUser, ldapPassword)
|
||||
if err != nil {
|
||||
log.Println("LDAP authentication error: ", err)
|
||||
return nil, errInvalidCredentials
|
||||
}
|
||||
return creds, nil
|
||||
}
|
||||
27
pkg/auth/ldap/config.go
Normal file
27
pkg/auth/ldap/config.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/pkg/env"
|
||||
)
|
||||
|
||||
func GetLDAPEnabled() bool {
|
||||
return strings.ToLower(env.Get(MCSLDAPEnabled, "off")) == "on"
|
||||
}
|
||||
22
pkg/auth/ldap/const.go
Normal file
22
pkg/auth/ldap/const.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package ldap
|
||||
|
||||
const (
|
||||
// const for ldap configuration
|
||||
MCSLDAPEnabled = "MCS_LDAP_ENABLED"
|
||||
)
|
||||
@@ -22,13 +22,14 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/minio/mcs/pkg/auth"
|
||||
"github.com/go-openapi/swag"
|
||||
)
|
||||
|
||||
// Authenticate validates websocket header and returns mcs jwt claims
|
||||
// GetTokenFromRequest returns a token from a http Request
|
||||
// either defined on a cookie `token` or on Authorization header.
|
||||
//
|
||||
// Authorization Header needs to be like "Authorization Bearer <jwt_token>"
|
||||
func Authenticate(r *http.Request) (*auth.DecryptedClaims, error) {
|
||||
func GetTokenFromRequest(r *http.Request) (*string, error) {
|
||||
// Get Auth token
|
||||
var reqToken string
|
||||
|
||||
@@ -46,11 +47,5 @@ func Authenticate(r *http.Request) (*auth.DecryptedClaims, error) {
|
||||
} else {
|
||||
reqToken = strings.TrimSpace(tokenCookie.Value)
|
||||
}
|
||||
|
||||
// Perform authentication before upgrading to a Websocket Connection
|
||||
claims, err := auth.JWTAuthenticate(reqToken)
|
||||
if err != nil {
|
||||
return nil, errors.New(http.StatusUnauthorized, err.Error())
|
||||
}
|
||||
return claims, nil
|
||||
return swag.String(reqToken), nil
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -20,6 +20,7 @@
|
||||
"@types/superagent": "^4.1.4",
|
||||
"@types/webpack-env": "^1.14.1",
|
||||
"@types/websocket": "^1.0.0",
|
||||
"ansi-to-react": "^6.0.5",
|
||||
"codemirror": "^5.52.2",
|
||||
"history": "^4.10.1",
|
||||
"local-storage-fallback": "^4.1.1",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Acme cloud storage"
|
||||
content="MinIO Console"
|
||||
/>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
|
||||
@@ -35,7 +35,7 @@ const isLoggedIn = () => {
|
||||
};
|
||||
|
||||
const mapState = (state: AppState) => ({
|
||||
loggedIn: state.system.loggedIn
|
||||
loggedIn: state.system.loggedIn,
|
||||
});
|
||||
|
||||
const connector = connect(mapState, { userLoggedIn });
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import storage from "local-storage-fallback";
|
||||
import request from "superagent";
|
||||
import get from "lodash/get";
|
||||
|
||||
export class API {
|
||||
invoke(method: string, url: string, data?: object) {
|
||||
@@ -23,8 +24,8 @@ export class API {
|
||||
return request(method, url)
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.send(data)
|
||||
.then(res => res.body)
|
||||
.catch(err => {
|
||||
.then((res) => res.body)
|
||||
.catch((err) => {
|
||||
// if we get unauthorized, kick out the user
|
||||
if (err.status === 401) {
|
||||
storage.removeItem("token");
|
||||
@@ -36,10 +37,16 @@ export class API {
|
||||
|
||||
onError(err: any) {
|
||||
if (err.status) {
|
||||
const errMessage: string =
|
||||
(err.response.body && err.response.body.error) ||
|
||||
err.status.toString(10);
|
||||
return Promise.reject(errMessage);
|
||||
const errMessage = get(
|
||||
err.response,
|
||||
"body.message",
|
||||
err.status.toString()
|
||||
);
|
||||
|
||||
const throwMessage =
|
||||
errMessage.charAt(0).toUpperCase() + errMessage.slice(1);
|
||||
|
||||
return Promise.reject(throwMessage);
|
||||
} else {
|
||||
return Promise.reject("Unknown error");
|
||||
}
|
||||
|
||||
@@ -14,7 +14,17 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
export const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
export const units = [
|
||||
"B",
|
||||
"KiB",
|
||||
"MiB",
|
||||
"GiB",
|
||||
"TiB",
|
||||
"PiB",
|
||||
"EiB",
|
||||
"ZiB",
|
||||
"YiB",
|
||||
];
|
||||
export const niceBytes = (x: string) => {
|
||||
let l = 0,
|
||||
n = parseInt(x, 10) || 0;
|
||||
@@ -38,3 +48,12 @@ export const setCookie = (name: string, val: string) => {
|
||||
document.cookie =
|
||||
name + "=" + value + "; expires=" + date.toUTCString() + "; path=/";
|
||||
};
|
||||
|
||||
// timeFromdate gets time string from date input
|
||||
export const timeFromDate = (d: Date) => {
|
||||
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
|
||||
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;
|
||||
let s = d.getSeconds() < 10 ? `0${d.getSeconds()}` : `${d.getSeconds()}`;
|
||||
|
||||
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ import Grid from "@material-ui/core/Grid";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { Button, LinearProgress } from "@material-ui/core";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
|
||||
import api from "../../../../common/api";
|
||||
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
|
||||
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
@@ -26,11 +27,12 @@ import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBo
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right"
|
||||
}
|
||||
textAlign: "right",
|
||||
},
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
interface IAddBucketProps {
|
||||
@@ -49,7 +51,7 @@ class AddBucket extends React.Component<IAddBucketProps, IAddBucketState> {
|
||||
state: IAddBucketState = {
|
||||
addLoading: false,
|
||||
addError: "",
|
||||
bucketName: ""
|
||||
bucketName: "",
|
||||
};
|
||||
|
||||
addRecord(event: React.FormEvent) {
|
||||
@@ -61,23 +63,23 @@ class AddBucket extends React.Component<IAddBucketProps, IAddBucketState> {
|
||||
this.setState({ addLoading: true }, () => {
|
||||
api
|
||||
.invoke("POST", "/api/v1/buckets", {
|
||||
name: bucketName
|
||||
name: bucketName,
|
||||
})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
this.setState(
|
||||
{
|
||||
addLoading: false,
|
||||
addError: ""
|
||||
addError: "",
|
||||
},
|
||||
() => {
|
||||
this.props.closeModalAndRefresh();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
addError: err,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -106,31 +108,29 @@ class AddBucket extends React.Component<IAddBucketProps, IAddBucketState> {
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
<InputBoxWrapper
|
||||
id="bucket-name"
|
||||
name="bucket-name"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ bucketName: e.target.value });
|
||||
}}
|
||||
label="Bucket Name"
|
||||
value={bucketName}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="bucket-name"
|
||||
name="bucket-name"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ bucketName: e.target.value });
|
||||
}}
|
||||
label="Bucket Name"
|
||||
value={bucketName}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
|
||||
@@ -30,49 +30,50 @@ import AddBucket from "./AddBucket";
|
||||
import DeleteBucket from "./DeleteBucket";
|
||||
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
|
||||
import { CreateIcon } from "../../../../icons";
|
||||
import { niceBytes } from "../../../../common/utils";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
seeMore: {
|
||||
marginTop: theme.spacing(3)
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
paper: {
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column"
|
||||
flexDirection: "column",
|
||||
},
|
||||
|
||||
addSideBar: {
|
||||
width: "320px",
|
||||
padding: "20px"
|
||||
padding: "20px",
|
||||
},
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
tableToolbar: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(0)
|
||||
paddingRight: theme.spacing(0),
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
"& tr": {
|
||||
"& th": {
|
||||
fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
actionsTray: {
|
||||
textAlign: "right",
|
||||
"& button": {
|
||||
marginLeft: 10
|
||||
}
|
||||
marginLeft: 10,
|
||||
},
|
||||
},
|
||||
searchField: {
|
||||
background: "#FFFFFF",
|
||||
padding: 12,
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 3px 6px #00000012"
|
||||
}
|
||||
boxShadow: "0px 3px 6px #00000012",
|
||||
},
|
||||
});
|
||||
|
||||
interface IListBucketsProps {
|
||||
@@ -108,7 +109,7 @@ class ListBuckets extends React.Component<
|
||||
rowsPerPage: 10,
|
||||
deleteOpen: false,
|
||||
selectedBucket: "",
|
||||
filterBuckets: ""
|
||||
filterBuckets: "",
|
||||
};
|
||||
|
||||
fetchRecords() {
|
||||
@@ -122,7 +123,7 @@ class ListBuckets extends React.Component<
|
||||
loading: false,
|
||||
records: res.buckets || [],
|
||||
totalRecords: !res.buckets ? 0 : res.total,
|
||||
error: ""
|
||||
error: "",
|
||||
});
|
||||
// if we get 0 results, and page > 0 , go down 1 page
|
||||
if (
|
||||
@@ -174,7 +175,7 @@ class ListBuckets extends React.Component<
|
||||
rowsPerPage,
|
||||
deleteOpen,
|
||||
selectedBucket,
|
||||
filterBuckets
|
||||
filterBuckets,
|
||||
} = this.state;
|
||||
|
||||
const offset = page * rowsPerPage;
|
||||
@@ -196,7 +197,7 @@ class ListBuckets extends React.Component<
|
||||
|
||||
const tableActions = [
|
||||
{ type: "view", to: `/buckets`, sendOnlyId: true },
|
||||
{ type: "delete", onClick: confirmDeleteBucket, sendOnlyId: true }
|
||||
{ type: "delete", onClick: confirmDeleteBucket, sendOnlyId: true },
|
||||
];
|
||||
|
||||
const displayParsedDate = (date: string) => {
|
||||
@@ -249,9 +250,9 @@ class ListBuckets extends React.Component<
|
||||
className={classes.searchField}
|
||||
id="search-resource"
|
||||
label=""
|
||||
onChange={val => {
|
||||
onChange={(val) => {
|
||||
this.setState({
|
||||
filterBuckets: val.target.value
|
||||
filterBuckets: val.target.value,
|
||||
});
|
||||
}}
|
||||
InputProps={{
|
||||
@@ -260,7 +261,7 @@ class ListBuckets extends React.Component<
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
)
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
@@ -269,7 +270,7 @@ class ListBuckets extends React.Component<
|
||||
startIcon={<CreateIcon />}
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
addScreenOpen: true
|
||||
addScreenOpen: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -287,8 +288,13 @@ class ListBuckets extends React.Component<
|
||||
{
|
||||
label: "Creation Date",
|
||||
elementKey: "creation_date",
|
||||
renderFunction: displayParsedDate
|
||||
}
|
||||
renderFunction: displayParsedDate,
|
||||
},
|
||||
{
|
||||
label: "Size",
|
||||
elementKey: "size",
|
||||
renderFunction: niceBytes,
|
||||
},
|
||||
]}
|
||||
isLoading={loading}
|
||||
records={filteredRecords}
|
||||
@@ -302,11 +308,11 @@ class ListBuckets extends React.Component<
|
||||
page: page,
|
||||
SelectProps: {
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true
|
||||
native: true,
|
||||
},
|
||||
onChangePage: handleChangePage,
|
||||
onChangeRowsPerPage: handleChangeRowsPerPage,
|
||||
ActionsComponent: MinTablePaginationActions
|
||||
ActionsComponent: MinTablePaginationActions,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -27,6 +27,7 @@ import TableBody from "@material-ui/core/TableBody";
|
||||
import Checkbox from "@material-ui/core/Checkbox";
|
||||
import Table from "@material-ui/core/Table";
|
||||
import { ArnList } from "../types";
|
||||
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
|
||||
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
|
||||
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
|
||||
@@ -34,19 +35,20 @@ import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapp
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
"& tr": {
|
||||
"& th": {
|
||||
fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right"
|
||||
}
|
||||
textAlign: "right",
|
||||
},
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
interface IAddEventProps {
|
||||
@@ -74,7 +76,7 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
|
||||
suffix: "",
|
||||
arn: "",
|
||||
selectedEvents: [],
|
||||
arnList: []
|
||||
arnList: [],
|
||||
};
|
||||
|
||||
addRecord(event: React.FormEvent) {
|
||||
@@ -91,25 +93,25 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
|
||||
arn: arn,
|
||||
events: selectedEvents,
|
||||
prefix: prefix,
|
||||
suffix: suffix
|
||||
suffix: suffix,
|
||||
},
|
||||
ignoreExisting: true
|
||||
ignoreExisting: true,
|
||||
})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
this.setState(
|
||||
{
|
||||
addLoading: false,
|
||||
addError: ""
|
||||
addError: "",
|
||||
},
|
||||
() => {
|
||||
this.props.closeModalAndRefresh();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
addError: err,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -127,7 +129,7 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
arnList: arns,
|
||||
addError: ""
|
||||
addError: "",
|
||||
});
|
||||
})
|
||||
.catch((err: any) => {
|
||||
@@ -149,13 +151,13 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
|
||||
selectedEvents,
|
||||
arnList,
|
||||
prefix,
|
||||
suffix
|
||||
suffix,
|
||||
} = this.state;
|
||||
|
||||
const events = [
|
||||
{ label: "PUT - Object Uploaded", value: "put" },
|
||||
{ label: "GET - Object accessed", value: "get" },
|
||||
{ label: "DELETE - Object Deleted", value: "delete" }
|
||||
{ label: "DELETE - Object Deleted", value: "delete" },
|
||||
];
|
||||
|
||||
const handleClick = (
|
||||
@@ -181,9 +183,9 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
|
||||
this.setState({ selectedEvents: newSelected });
|
||||
};
|
||||
|
||||
const arnValues = arnList.map(arnConstant => ({
|
||||
const arnValues = arnList.map((arnConstant) => ({
|
||||
label: arnConstant,
|
||||
value: arnConstant
|
||||
value: arnConstant,
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -204,89 +206,91 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
<SelectWrapper
|
||||
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
|
||||
this.setState({ arn: e.target.value as string });
|
||||
}}
|
||||
id="select-access-policy"
|
||||
name="select-access-policy"
|
||||
label={"ARN"}
|
||||
value={arn}
|
||||
options={arnValues}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<SelectWrapper
|
||||
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
|
||||
this.setState({ arn: e.target.value as string });
|
||||
}}
|
||||
id="select-access-policy"
|
||||
name="select-access-policy"
|
||||
label={"ARN"}
|
||||
value={arn}
|
||||
options={arnValues}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Table size="medium">
|
||||
<TableHead className={classes.minTableHeader}>
|
||||
<TableRow>
|
||||
<TableCell>Select</TableCell>
|
||||
<TableCell>Event</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{events.map(row => (
|
||||
<TableRow
|
||||
key={`group-${row.value}`}
|
||||
onClick={event => handleClick(event, row.value)}
|
||||
>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
value={row.value}
|
||||
color="primary"
|
||||
inputProps={{
|
||||
"aria-label": "secondary checkbox"
|
||||
}}
|
||||
onChange={event => handleClick(event, row.value)}
|
||||
checked={selectedEvents.includes(row.value)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.wrapCell}>
|
||||
{row.label}
|
||||
</TableCell>
|
||||
<Grid item xs={12}>
|
||||
<Table size="medium">
|
||||
<TableHead className={classes.minTableHeader}>
|
||||
<TableRow>
|
||||
<TableCell>Select</TableCell>
|
||||
<TableCell>Event</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="prefix-input"
|
||||
name="prefix-input"
|
||||
label="Prefix"
|
||||
value={prefix}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ prefix: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="suffix-input"
|
||||
name="suffix-input"
|
||||
label="Suffix"
|
||||
value={suffix}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ suffix: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{events.map((row) => (
|
||||
<TableRow
|
||||
key={`group-${row.value}`}
|
||||
onClick={(event) => handleClick(event, row.value)}
|
||||
>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
value={row.value}
|
||||
color="primary"
|
||||
inputProps={{
|
||||
"aria-label": "secondary checkbox",
|
||||
}}
|
||||
onChange={(event) => handleClick(event, row.value)}
|
||||
checked={selectedEvents.includes(row.value)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.wrapCell}>
|
||||
{row.label}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="prefix-input"
|
||||
name="prefix-input"
|
||||
label="Prefix"
|
||||
value={prefix}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ prefix: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="suffix-input"
|
||||
name="suffix-input"
|
||||
label="Suffix"
|
||||
value={suffix}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ suffix: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import React from "react";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import get from "lodash/get";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
LinearProgress
|
||||
LinearProgress,
|
||||
} from "@material-ui/core";
|
||||
import api from "../../../../common/api";
|
||||
import { BucketEvent, BucketList } from "../types";
|
||||
@@ -32,8 +33,8 @@ import Typography from "@material-ui/core/Typography";
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
}
|
||||
color: "red",
|
||||
},
|
||||
});
|
||||
|
||||
interface IDeleteEventProps {
|
||||
@@ -55,7 +56,7 @@ class DeleteEvent extends React.Component<
|
||||
> {
|
||||
state: IDeleteEventState = {
|
||||
deleteLoading: false,
|
||||
deleteError: ""
|
||||
deleteError: "",
|
||||
};
|
||||
|
||||
removeRecord() {
|
||||
@@ -69,29 +70,34 @@ class DeleteEvent extends React.Component<
|
||||
}
|
||||
|
||||
this.setState({ deleteLoading: true }, () => {
|
||||
const events = get(bucketEvent, "events", []);
|
||||
const prefix = get(bucketEvent, "prefix", "");
|
||||
const suffix = get(bucketEvent, "suffix", "");
|
||||
api
|
||||
.invoke(
|
||||
"DELETE",
|
||||
`/api/v1/buckets/${selectedBucket}/events/${bucketEvent.id}`,
|
||||
`/api/v1/buckets/${selectedBucket}/events/${bucketEvent.arn}`,
|
||||
{
|
||||
name: selectedBucket
|
||||
events,
|
||||
prefix,
|
||||
suffix,
|
||||
}
|
||||
)
|
||||
.then((res: BucketList) => {
|
||||
this.setState(
|
||||
{
|
||||
deleteLoading: false,
|
||||
deleteError: ""
|
||||
deleteError: "",
|
||||
},
|
||||
() => {
|
||||
this.props.closeDeleteModalAndRefresh(true);
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.setState({
|
||||
deleteLoading: false,
|
||||
deleteError: err
|
||||
deleteError: err,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import Grid from "@material-ui/core/Grid";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { Button, LinearProgress } from "@material-ui/core";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
|
||||
import api from "../../../../common/api";
|
||||
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
|
||||
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
|
||||
@@ -25,8 +26,9 @@ import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapp
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
}
|
||||
color: "red",
|
||||
},
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
interface ISetAccessPolicyProps {
|
||||
@@ -49,7 +51,7 @@ class SetAccessPolicy extends React.Component<
|
||||
state: ISetAccessPolicyState = {
|
||||
addLoading: false,
|
||||
addError: "",
|
||||
accessPolicy: ""
|
||||
accessPolicy: "",
|
||||
};
|
||||
|
||||
addRecord(event: React.FormEvent) {
|
||||
@@ -62,23 +64,23 @@ class SetAccessPolicy extends React.Component<
|
||||
this.setState({ addLoading: true }, () => {
|
||||
api
|
||||
.invoke("PUT", `/api/v1/buckets/${bucketName}/set-policy`, {
|
||||
access: accessPolicy
|
||||
access: accessPolicy,
|
||||
})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
this.setState(
|
||||
{
|
||||
addLoading: false,
|
||||
addError: ""
|
||||
addError: "",
|
||||
},
|
||||
() => {
|
||||
this.props.closeModalAndRefresh();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
addError: err,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -105,34 +107,33 @@ class SetAccessPolicy extends React.Component<
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
<SelectWrapper
|
||||
value={accessPolicy}
|
||||
label="Access Policy"
|
||||
id="select-access-policy"
|
||||
name="select-access-policy"
|
||||
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
|
||||
this.setState({ accessPolicy: e.target.value as string });
|
||||
}}
|
||||
options={[
|
||||
{ value: "PRIVATE", label: "Private" },
|
||||
{ value: "PUBLIC", label: "Public" },
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<SelectWrapper
|
||||
value={accessPolicy}
|
||||
label="Access Policy"
|
||||
id="select-access-policy"
|
||||
name="select-access-policy"
|
||||
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
|
||||
this.setState({ accessPolicy: e.target.value as string });
|
||||
}}
|
||||
options={[
|
||||
{ value: "PRIVATE", label: "Private" },
|
||||
{ value: "PUBLIC", label: "Public" }
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Button
|
||||
|
||||
@@ -32,50 +32,50 @@ import TableWrapper from "../../Common/TableWrapper/TableWrapper";
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
seeMore: {
|
||||
marginTop: theme.spacing(3)
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
paper: {
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column"
|
||||
flexDirection: "column",
|
||||
},
|
||||
|
||||
addSideBar: {
|
||||
width: "320px",
|
||||
padding: "20px"
|
||||
padding: "20px",
|
||||
},
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
tableToolbar: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(0)
|
||||
paddingRight: theme.spacing(0),
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
"& tr": {
|
||||
"& th": {
|
||||
fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
actionsTray: {
|
||||
textAlign: "right",
|
||||
"& button": {
|
||||
marginLeft: 10
|
||||
}
|
||||
marginLeft: 10,
|
||||
},
|
||||
},
|
||||
searchField: {
|
||||
background: "#FFFFFF",
|
||||
padding: 12,
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 3px 6px #00000012"
|
||||
boxShadow: "0px 3px 6px #00000012",
|
||||
},
|
||||
noRecords: {
|
||||
lineHeight: "24px",
|
||||
textAlign: "center",
|
||||
padding: "20px"
|
||||
}
|
||||
padding: "20px",
|
||||
},
|
||||
});
|
||||
|
||||
interface IViewBucketProps {
|
||||
@@ -113,7 +113,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
addScreenOpen: false,
|
||||
deleteOpen: false,
|
||||
selectedBucket: "",
|
||||
selectedEvent: null
|
||||
selectedEvent: null,
|
||||
};
|
||||
|
||||
fetchEvents() {
|
||||
@@ -131,7 +131,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
loading: false,
|
||||
records: events || [],
|
||||
totalRecords: total,
|
||||
error: ""
|
||||
error: "",
|
||||
});
|
||||
// if we get 0 results, and page > 0 , go down 1 page
|
||||
if ((!events || res.events.length === 0) && page > 0) {
|
||||
@@ -169,7 +169,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
.then((res: BucketInfo) => {
|
||||
this.setState({ info: res });
|
||||
})
|
||||
.catch(err => {});
|
||||
.catch((err) => {});
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -191,7 +191,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
rowsPerPage,
|
||||
deleteOpen,
|
||||
addScreenOpen,
|
||||
selectedEvent
|
||||
selectedEvent,
|
||||
} = this.state;
|
||||
|
||||
const offset = page * rowsPerPage;
|
||||
@@ -228,21 +228,26 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<AddEvent
|
||||
open={addScreenOpen}
|
||||
selectedBucket={bucketName}
|
||||
closeModalAndRefresh={() => {
|
||||
this.setState({ addScreenOpen: false });
|
||||
this.fetchEvents();
|
||||
}}
|
||||
/>
|
||||
<SetAccessPolicy
|
||||
bucketName={bucketName}
|
||||
open={setAccessPolicyScreenOpen}
|
||||
closeModalAndRefresh={() => {
|
||||
this.closeAddModalAndRefresh();
|
||||
}}
|
||||
/>
|
||||
{addScreenOpen && (
|
||||
<AddEvent
|
||||
open={addScreenOpen}
|
||||
selectedBucket={bucketName}
|
||||
closeModalAndRefresh={() => {
|
||||
this.setState({ addScreenOpen: false });
|
||||
this.fetchEvents();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{setAccessPolicyScreenOpen && (
|
||||
<SetAccessPolicy
|
||||
bucketName={bucketName}
|
||||
open={setAccessPolicyScreenOpen}
|
||||
closeModalAndRefresh={() => {
|
||||
this.closeAddModalAndRefresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">
|
||||
@@ -261,7 +266,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
setAccessPolicyScreenOpen: true
|
||||
setAccessPolicyScreenOpen: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -285,7 +290,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
startIcon={<CreateIcon />}
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
addScreenOpen: true
|
||||
addScreenOpen: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -303,10 +308,10 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
{
|
||||
label: "Events",
|
||||
elementKey: "events",
|
||||
renderFunction: eventsDisplay
|
||||
renderFunction: eventsDisplay,
|
||||
},
|
||||
{ label: "Prefix", elementKey: "prefix" },
|
||||
{ label: "Suffix", elementKey: "suffix" }
|
||||
{ label: "Suffix", elementKey: "suffix" },
|
||||
]}
|
||||
isLoading={loading}
|
||||
records={filteredRecords}
|
||||
@@ -320,11 +325,11 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
page: page,
|
||||
SelectProps: {
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true
|
||||
native: true,
|
||||
},
|
||||
onChangePage: handleChangePage,
|
||||
onChangeRowsPerPage: handleChangeRowsPerPage,
|
||||
ActionsComponent: MinTablePaginationActions
|
||||
ActionsComponent: MinTablePaginationActions,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -13,19 +13,13 @@
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
createRef,
|
||||
ChangeEvent,
|
||||
useCallback
|
||||
} from "react";
|
||||
import React, { useState, useEffect, createRef, ChangeEvent } from "react";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import get from "lodash/get";
|
||||
import InputBoxWrapper from "../InputBoxWrapper/InputBoxWrapper";
|
||||
import { InputLabel, Tooltip } from "@material-ui/core";
|
||||
import { fieldBasic } from "../common/styleLibrary";
|
||||
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
|
||||
import HelpIcon from "@material-ui/icons/Help";
|
||||
|
||||
interface ICSVMultiSelector {
|
||||
@@ -40,9 +34,10 @@ interface ICSVMultiSelector {
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...fieldBasic,
|
||||
...tooltipHelper,
|
||||
inputLabel: {
|
||||
...fieldBasic.inputLabel,
|
||||
marginBottom: 10
|
||||
width: 116,
|
||||
},
|
||||
inputContainer: {
|
||||
height: 150,
|
||||
@@ -50,11 +45,10 @@ const styles = (theme: Theme) =>
|
||||
padding: 15,
|
||||
position: "relative",
|
||||
border: "1px solid #c4c4c4",
|
||||
marginBottom: 10
|
||||
},
|
||||
labelContainer: {
|
||||
display: "flex"
|
||||
}
|
||||
display: "flex",
|
||||
},
|
||||
});
|
||||
|
||||
const CSVMultiSelector = ({
|
||||
@@ -63,26 +57,30 @@ const CSVMultiSelector = ({
|
||||
label,
|
||||
tooltip = "",
|
||||
onChange,
|
||||
classes
|
||||
classes,
|
||||
}: ICSVMultiSelector) => {
|
||||
const [currentElements, setCurrentElements] = useState<string[]>([""]);
|
||||
const bottomList = createRef<HTMLDivElement>();
|
||||
|
||||
// Use effect to get the initial values from props
|
||||
useCallback(() => {
|
||||
if (currentElements.length === 1 && currentElements[0] === "") {
|
||||
const elementsSplitted = elements.split(",");
|
||||
if (elementsSplitted[elementsSplitted.length - 1].trim() !== "") {
|
||||
elementsSplitted.push("");
|
||||
}
|
||||
setCurrentElements(elementsSplitted);
|
||||
useEffect(() => {
|
||||
if (
|
||||
currentElements.length === 1 &&
|
||||
currentElements[0] === "" &&
|
||||
elements &&
|
||||
elements !== ""
|
||||
) {
|
||||
const elementsSplit = elements.split(",");
|
||||
elementsSplit.push("");
|
||||
|
||||
setCurrentElements(elementsSplit);
|
||||
}
|
||||
}, [elements, setCurrentElements, currentElements]);
|
||||
}, [elements, currentElements]);
|
||||
|
||||
// Use effect to send new values to onChange
|
||||
useEffect(() => {
|
||||
const elementsString = currentElements
|
||||
.filter(element => element.trim() !== "")
|
||||
.filter((element) => element.trim() !== "")
|
||||
.join(",");
|
||||
onChange(elementsString);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -131,18 +129,20 @@ const CSVMultiSelector = ({
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12} className={classes.fieldContainer}>
|
||||
<InputLabel className={classes.inputLabel}>{label}</InputLabel>
|
||||
<InputLabel className={classes.inputLabel}>
|
||||
<span>{label}</span>
|
||||
{tooltip !== "" && (
|
||||
<div className={classes.tooltipContainer}>
|
||||
<Tooltip title={tooltip} placement="top-start">
|
||||
<HelpIcon className={classes.tooltip} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</InputLabel>
|
||||
<Grid item xs={12} className={classes.inputContainer}>
|
||||
{inputs}
|
||||
<div ref={bottomList} />
|
||||
</Grid>
|
||||
{tooltip !== "" && (
|
||||
<div className={classes.tooltipContainer}>
|
||||
<Tooltip title={tooltip} placement="left">
|
||||
<HelpIcon />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -19,16 +19,16 @@ import {
|
||||
InputLabel,
|
||||
TextField,
|
||||
TextFieldProps,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
import { OutlinedInputProps } from "@material-ui/core/OutlinedInput";
|
||||
import {
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Theme,
|
||||
withStyles
|
||||
withStyles,
|
||||
} from "@material-ui/core/styles";
|
||||
import { fieldBasic } from "../common/styleLibrary";
|
||||
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
|
||||
import HelpIcon from "@material-ui/icons/Help";
|
||||
|
||||
interface InputBoxProps {
|
||||
@@ -49,22 +49,23 @@ interface InputBoxProps {
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...fieldBasic,
|
||||
...tooltipHelper,
|
||||
textBoxContainer: {
|
||||
flexGrow: 1
|
||||
}
|
||||
flexGrow: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const inputStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
borderColor: "#393939",
|
||||
borderRadius: 0
|
||||
borderRadius: 0,
|
||||
},
|
||||
input: {
|
||||
padding: "11px 20px",
|
||||
color: "#393939",
|
||||
fontSize: 14
|
||||
}
|
||||
fontSize: 14,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -91,16 +92,24 @@ const InputBoxWrapper = ({
|
||||
multiline = false,
|
||||
tooltip = "",
|
||||
index = 0,
|
||||
classes
|
||||
classes,
|
||||
}: InputBoxProps) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12} className={classes.fieldContainer}>
|
||||
{label !== "" && (
|
||||
<InputLabel htmlFor={id} className={classes.inputLabel}>
|
||||
{label}
|
||||
<span>{label}</span>
|
||||
{tooltip !== "" && (
|
||||
<div className={classes.tooltipContainer}>
|
||||
<Tooltip title={tooltip} placement="top-start">
|
||||
<HelpIcon className={classes.tooltip} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</InputLabel>
|
||||
)}
|
||||
|
||||
<div className={classes.textBoxContainer}>
|
||||
<InputField
|
||||
className={classes.boxDesign}
|
||||
@@ -117,13 +126,6 @@ const InputBoxWrapper = ({
|
||||
inputProps={{ "data-index": index }}
|
||||
/>
|
||||
</div>
|
||||
{tooltip !== "" && (
|
||||
<div className={classes.tooltipContainer}>
|
||||
<Tooltip title={tooltip} placement="left">
|
||||
<HelpIcon />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -23,9 +23,9 @@ import {
|
||||
createStyles,
|
||||
Theme,
|
||||
withStyles,
|
||||
makeStyles
|
||||
makeStyles,
|
||||
} from "@material-ui/core/styles";
|
||||
import { fieldBasic } from "../common/styleLibrary";
|
||||
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
|
||||
import HelpIcon from "@material-ui/icons/Help";
|
||||
|
||||
export interface SelectorTypes {
|
||||
@@ -48,22 +48,23 @@ interface RadioGroupProps {
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...fieldBasic,
|
||||
...tooltipHelper,
|
||||
radioBoxContainer: {
|
||||
flexGrow: 1
|
||||
}
|
||||
flexGrow: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const radioStyles = makeStyles({
|
||||
root: {
|
||||
"&:hover": {
|
||||
backgroundColor: "transparent"
|
||||
}
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
borderRadius: "100%",
|
||||
width: 14,
|
||||
height: 14,
|
||||
border: "1px solid #000"
|
||||
border: "1px solid #000",
|
||||
},
|
||||
checkedIcon: {
|
||||
borderRadius: "100%",
|
||||
@@ -81,9 +82,9 @@ const radioStyles = makeStyles({
|
||||
position: "absolute",
|
||||
backgroundColor: "#000",
|
||||
top: 2,
|
||||
left: 2
|
||||
}
|
||||
}
|
||||
left: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const RadioButton = (props: RadioProps) => {
|
||||
@@ -110,14 +111,22 @@ export const RadioGroupSelector = ({
|
||||
onChange,
|
||||
tooltip = "",
|
||||
classes,
|
||||
displayInColumn = false
|
||||
displayInColumn = false,
|
||||
}: RadioGroupProps) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12} className={classes.fieldContainer}>
|
||||
<InputLabel htmlFor={id} className={classes.inputLabel}>
|
||||
{label}
|
||||
<span>{label}</span>
|
||||
{tooltip !== "" && (
|
||||
<div className={classes.tooltipContainer}>
|
||||
<Tooltip title={tooltip} placement="top-start">
|
||||
<HelpIcon className={classes.tooltip} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</InputLabel>
|
||||
|
||||
<div className={classes.radioBoxContainer}>
|
||||
<RadioGroup
|
||||
aria-label={id}
|
||||
@@ -127,7 +136,7 @@ export const RadioGroupSelector = ({
|
||||
onChange={onChange}
|
||||
row={!displayInColumn}
|
||||
>
|
||||
{selectorOptions.map(selectorOption => {
|
||||
{selectorOptions.map((selectorOption) => {
|
||||
return (
|
||||
<FormControlLabel
|
||||
key={`rd-${name}-${selectorOption.value}`}
|
||||
@@ -139,13 +148,6 @@ export const RadioGroupSelector = ({
|
||||
})}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
{tooltip !== "" && (
|
||||
<div>
|
||||
<Tooltip title={tooltip} placement="left">
|
||||
<HelpIcon />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -20,10 +20,12 @@ import {
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
InputBase
|
||||
InputBase,
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { fieldBasic } from "../common/styleLibrary";
|
||||
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
|
||||
import HelpIcon from "@material-ui/icons/Help";
|
||||
|
||||
interface selectorTypes {
|
||||
label: string;
|
||||
@@ -36,6 +38,7 @@ interface SelectProps {
|
||||
label: string;
|
||||
id: string;
|
||||
name: string;
|
||||
tooltip?: string;
|
||||
onChange: (
|
||||
e: React.ChangeEvent<{ name?: string | undefined; value: unknown }>
|
||||
) => void;
|
||||
@@ -44,15 +47,20 @@ interface SelectProps {
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...fieldBasic
|
||||
...fieldBasic,
|
||||
...tooltipHelper,
|
||||
inputLabel: {
|
||||
...fieldBasic.inputLabel,
|
||||
width: 116,
|
||||
},
|
||||
});
|
||||
|
||||
const SelectStyled = withStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
"label + &": {
|
||||
marginTop: theme.spacing(3)
|
||||
}
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
},
|
||||
input: {
|
||||
borderRadius: 0,
|
||||
@@ -62,12 +70,12 @@ const SelectStyled = withStyles((theme: Theme) =>
|
||||
padding: "11px 20px",
|
||||
border: "1px solid #c4c4c4",
|
||||
"&:hover": {
|
||||
borderColor: "#393939"
|
||||
borderColor: "#393939",
|
||||
},
|
||||
"&:focus": {
|
||||
backgroundColor: "#fff"
|
||||
}
|
||||
}
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
},
|
||||
})
|
||||
)(InputBase);
|
||||
|
||||
@@ -78,13 +86,21 @@ const SelectWrapper = ({
|
||||
onChange,
|
||||
options,
|
||||
label,
|
||||
value
|
||||
tooltip = "",
|
||||
value,
|
||||
}: SelectProps) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12} className={classes.fieldContainer}>
|
||||
<InputLabel htmlFor={id} className={classes.inputLabel}>
|
||||
{label}
|
||||
<span>{label}</span>
|
||||
{tooltip !== "" && (
|
||||
<div className={classes.tooltipContainer}>
|
||||
<Tooltip title={tooltip} placement="top-start">
|
||||
<HelpIcon className={classes.tooltip} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</InputLabel>
|
||||
<FormControl variant="outlined" fullWidth>
|
||||
<Select
|
||||
@@ -94,7 +110,7 @@ const SelectWrapper = ({
|
||||
onChange={onChange}
|
||||
input={<SelectStyled />}
|
||||
>
|
||||
{options.map(option => (
|
||||
{options.map((option) => (
|
||||
<MenuItem
|
||||
value={option.value}
|
||||
key={`select-${name}-${option.label}`}
|
||||
|
||||
@@ -19,17 +19,45 @@
|
||||
export const fieldBasic = {
|
||||
inputLabel: {
|
||||
fontWeight: 500,
|
||||
marginRight: 16,
|
||||
minWidth: 90,
|
||||
marginRight: 10,
|
||||
width: 100,
|
||||
fontSize: 14,
|
||||
color: "#393939"
|
||||
color: "#393939",
|
||||
textAlign: "right" as const,
|
||||
display: "flex",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
justifyContent: "flex-end",
|
||||
"& span": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
},
|
||||
fieldContainer: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: 10
|
||||
marginBottom: 10,
|
||||
},
|
||||
tooltipContainer: {
|
||||
marginLeft: 5
|
||||
}
|
||||
marginLeft: 5,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
};
|
||||
|
||||
export const modalBasic = {
|
||||
formScrollable: {
|
||||
maxHeight: "calc(100vh - 300px)" as const,
|
||||
overflowY: "auto" as const,
|
||||
marginBottom: 25,
|
||||
},
|
||||
formSlider: {
|
||||
marginLeft: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const tooltipHelper = {
|
||||
tooltip: {
|
||||
fontSize: 18,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
TableRow,
|
||||
Paper,
|
||||
Grid,
|
||||
Checkbox
|
||||
Checkbox,
|
||||
} from "@material-ui/core";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { TablePaginationActionsProps } from "@material-ui/core/TablePagination/TablePaginationActions";
|
||||
@@ -84,25 +84,25 @@ const borderColor = "#eaeaea";
|
||||
const rowText = {
|
||||
fontWeight: 400,
|
||||
fontSize: 14,
|
||||
borderColor: borderColor
|
||||
borderColor: borderColor,
|
||||
};
|
||||
|
||||
const checkBoxBasic = {
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: 3
|
||||
borderRadius: 3,
|
||||
};
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
dialogContainer: {
|
||||
padding: "12px 26px 22px"
|
||||
padding: "12px 26px 22px",
|
||||
},
|
||||
paper: {
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column",
|
||||
padding: "19px 38px"
|
||||
padding: "19px 38px",
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
@@ -111,43 +111,43 @@ const styles = (theme: Theme) =>
|
||||
fontWeight: 700,
|
||||
fontSize: 14,
|
||||
paddingBottom: 15,
|
||||
borderColor: borderColor
|
||||
}
|
||||
}
|
||||
borderColor: borderColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
rowUnselected: {
|
||||
...rowText
|
||||
...rowText,
|
||||
},
|
||||
rowSelected: {
|
||||
...rowText,
|
||||
color: "#201763"
|
||||
color: "#201763",
|
||||
},
|
||||
paginatorContainer: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
padding: "5px 38px"
|
||||
padding: "5px 38px",
|
||||
},
|
||||
checkBoxHeader: {
|
||||
"&.MuiTableCell-paddingCheckbox": {
|
||||
paddingBottom: 9
|
||||
}
|
||||
paddingBottom: 9,
|
||||
},
|
||||
},
|
||||
actionsContainer: {
|
||||
width: 120,
|
||||
borderColor: borderColor
|
||||
borderColor: borderColor,
|
||||
},
|
||||
paginatorComponent: {
|
||||
borderBottom: 0
|
||||
borderBottom: 0,
|
||||
},
|
||||
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #d0d0d0" },
|
||||
checkedIcon: {
|
||||
...checkBoxBasic,
|
||||
border: "1px solid #201763",
|
||||
backgroundColor: "#201763"
|
||||
backgroundColor: "#201763",
|
||||
},
|
||||
checkBoxRow: {
|
||||
borderColor: borderColor
|
||||
}
|
||||
borderColor: borderColor,
|
||||
},
|
||||
});
|
||||
|
||||
// Function that renders Title Columns
|
||||
@@ -221,7 +221,7 @@ const TableWrapper = ({
|
||||
idField,
|
||||
classes,
|
||||
stickyHeader = false,
|
||||
paginatorConfig
|
||||
paginatorConfig,
|
||||
}: TableWrapperProps) => {
|
||||
return (
|
||||
<Grid item xs={12}>
|
||||
|
||||
@@ -17,40 +17,67 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import { IElementValue, KVField } from "./types";
|
||||
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
|
||||
import { IElementValue, KVField } from "./types";
|
||||
import CSVMultiSelector from "../Common/FormComponents/CSVMultiSelector/CSVMultiSelector";
|
||||
|
||||
interface IConfGenericProps {
|
||||
onChange: (newValue: IElementValue[]) => void;
|
||||
fields: KVField[];
|
||||
defaultVals?: IElementValue[];
|
||||
classes: any;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => createStyles({});
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
// Function to get defined values,
|
||||
//we make this because the backed sometimes don't return all the keys when there is an initial configuration
|
||||
export const valueDef = (
|
||||
key: string,
|
||||
type: string,
|
||||
defaults: IElementValue[]
|
||||
) => {
|
||||
let defValue = type === "on|off" ? "false" : "";
|
||||
|
||||
if (defaults.length > 0) {
|
||||
const storedConfig = defaults.find((element) => element.key === key);
|
||||
|
||||
if (storedConfig) {
|
||||
defValue = storedConfig.value;
|
||||
}
|
||||
}
|
||||
|
||||
return defValue;
|
||||
};
|
||||
|
||||
const ConfTargetGeneric = ({
|
||||
onChange,
|
||||
fields,
|
||||
classes
|
||||
defaultVals,
|
||||
classes,
|
||||
}: IConfGenericProps) => {
|
||||
const [valueHolder, setValueHolder] = useState<IElementValue[]>([]);
|
||||
const fieldsElements = !fields ? [] : fields;
|
||||
const defValList = !defaultVals ? [] : defaultVals;
|
||||
|
||||
// Effect to create all the values to hold
|
||||
useEffect(() => {
|
||||
const values: IElementValue[] = [];
|
||||
fields.forEach(field => {
|
||||
fields.forEach((field) => {
|
||||
const stateInsert: IElementValue = {
|
||||
key: field.name,
|
||||
value: field.type === "on|off" ? "false" : ""
|
||||
value: valueDef(field.name, field.type, defValList),
|
||||
};
|
||||
values.push(stateInsert);
|
||||
});
|
||||
|
||||
setValueHolder(values);
|
||||
}, [fields]);
|
||||
}, [fields, defaultVals]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(valueHolder);
|
||||
@@ -79,7 +106,7 @@ const ConfTargetGeneric = ({
|
||||
}
|
||||
selectorOptions={[
|
||||
{ label: "On", value: "true" },
|
||||
{ label: "Off", value: "false" }
|
||||
{ label: "Off", value: "false" },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
@@ -114,18 +141,14 @@ const ConfTargetGeneric = ({
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
{fieldsElements.map((field, item) => (
|
||||
<React.Fragment key={field.name}>
|
||||
<Grid item xs={12}>
|
||||
{fieldDefinition(field, item)}
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
<Grid xs={12} item className={classes.formScrollable}>
|
||||
{fieldsElements.map((field, item) => (
|
||||
<React.Fragment key={field.name}>
|
||||
<Grid item xs={12}>
|
||||
{fieldDefinition(field, item)}
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -35,38 +35,37 @@ interface IListConfiguration {
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
strongText: {
|
||||
fontWeight: 700
|
||||
fontWeight: 700,
|
||||
},
|
||||
keyName: {
|
||||
marginLeft: 5
|
||||
marginLeft: 5,
|
||||
},
|
||||
actionsTray: {
|
||||
textAlign: "right",
|
||||
"& button": {
|
||||
marginLeft: 10
|
||||
}
|
||||
marginLeft: 10,
|
||||
},
|
||||
},
|
||||
searchField: {
|
||||
background: "#FFFFFF",
|
||||
padding: 12,
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 3px 6px #00000012"
|
||||
boxShadow: "0px 3px 6px #00000012",
|
||||
},
|
||||
iconText: {
|
||||
lineHeight: "24px"
|
||||
}
|
||||
lineHeight: "24px",
|
||||
},
|
||||
});
|
||||
|
||||
const ConfigurationsList = ({ classes }: IListConfiguration) => {
|
||||
const [editScreenOpen, setEditScreenOpen] = useState(false);
|
||||
const [selectedConfiguration, setSelectedConfiguration] = useState({
|
||||
configuration_id: "",
|
||||
configuration_label: ""
|
||||
configuration_label: "",
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
@@ -82,12 +81,12 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
|
||||
setSelectedConfiguration(element);
|
||||
setEditScreenOpen(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const filteredRecords: IConfigurationElement[] = configurationElements.filter(
|
||||
elementItem =>
|
||||
(elementItem) =>
|
||||
elementItem.configuration_id
|
||||
.toLocaleLowerCase()
|
||||
.includes(filter.toLocaleLowerCase())
|
||||
@@ -99,7 +98,6 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
|
||||
<EditConfiguration
|
||||
open={editScreenOpen}
|
||||
closeModalAndRefresh={() => {
|
||||
setIsLoading(true);
|
||||
setEditScreenOpen(false);
|
||||
}}
|
||||
selectedConfiguration={selectedConfiguration}
|
||||
@@ -119,7 +117,7 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
|
||||
className={classes.searchField}
|
||||
id="search-resource"
|
||||
label=""
|
||||
onChange={event => {
|
||||
onChange={(event) => {
|
||||
setFilter(event.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
@@ -128,7 +126,7 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
)
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -139,9 +137,9 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
|
||||
<TableWrapper
|
||||
itemActions={tableActions}
|
||||
columns={[
|
||||
{ label: "Configuration", elementKey: "configuration_id" }
|
||||
{ label: "Configuration", elementKey: "configuration_id" },
|
||||
]}
|
||||
isLoading={isLoading}
|
||||
isLoading={false}
|
||||
records={filteredRecords}
|
||||
entityName="Configurations"
|
||||
idField="configuration_id"
|
||||
|
||||
@@ -44,29 +44,29 @@ interface IWebhook {
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
strongText: {
|
||||
fontWeight: 700
|
||||
fontWeight: 700,
|
||||
},
|
||||
keyName: {
|
||||
marginLeft: 5
|
||||
marginLeft: 5,
|
||||
},
|
||||
actionsTray: {
|
||||
textAlign: "right",
|
||||
"& button": {
|
||||
marginLeft: 10
|
||||
}
|
||||
marginLeft: 10,
|
||||
},
|
||||
},
|
||||
searchField: {
|
||||
background: "#FFFFFF",
|
||||
padding: 12,
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 3px 6px #00000012"
|
||||
boxShadow: "0px 3px 6px #00000012",
|
||||
},
|
||||
iconText: {
|
||||
lineHeight: "24px"
|
||||
}
|
||||
lineHeight: "24px",
|
||||
},
|
||||
});
|
||||
|
||||
const panels = {
|
||||
@@ -77,8 +77,8 @@ const panels = {
|
||||
apiURL: "",
|
||||
configuration: {
|
||||
configuration_id: "logger_webhook",
|
||||
configuration_label: "Logger Webhook"
|
||||
}
|
||||
configuration_label: "Logger Webhook",
|
||||
},
|
||||
},
|
||||
audit: {
|
||||
main: "audit",
|
||||
@@ -87,9 +87,9 @@ const panels = {
|
||||
apiURL: "",
|
||||
configuration: {
|
||||
configuration_id: "audit_webhook",
|
||||
configuration_label: "Audit Webhook"
|
||||
}
|
||||
}
|
||||
configuration_label: "Audit Webhook",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
|
||||
@@ -107,15 +107,15 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filteredRecords: IWebhook[] = webhooks.filter(elementItem =>
|
||||
const filteredRecords: IWebhook[] = webhooks.filter((elementItem) =>
|
||||
elementItem.name.toLocaleLowerCase().includes(filter.toLocaleLowerCase())
|
||||
);
|
||||
|
||||
const tableActions = [
|
||||
{
|
||||
type: "edit",
|
||||
onClick: () => {}
|
||||
}
|
||||
onClick: () => {},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -144,7 +144,7 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
|
||||
className={classes.searchField}
|
||||
id="search-resource"
|
||||
label=""
|
||||
onChange={event => {
|
||||
onChange={(event) => {
|
||||
setFilter(event.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
@@ -153,7 +153,7 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
)
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -21,13 +21,17 @@ import Grid from "@material-ui/core/Grid";
|
||||
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
|
||||
import { IElementValue } from "../types";
|
||||
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
|
||||
|
||||
interface IConfMySqlProps {
|
||||
onChange: (newValue: IElementValue[]) => void;
|
||||
classes: any;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => createStyles({});
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
|
||||
//Local States
|
||||
@@ -88,7 +92,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
|
||||
{ key: "format", value: format },
|
||||
{ key: "queue_dir", value: queueDir },
|
||||
{ key: "queue_limit", value: queueLimit },
|
||||
{ key: "comment", value: comment }
|
||||
{ key: "comment", value: comment },
|
||||
];
|
||||
|
||||
onChange(formValues);
|
||||
@@ -101,7 +105,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
|
||||
}, [user, dbName, password, port, host, setDsnString, configToDsnString]);
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid container className={classes.formScrollable}>
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -119,7 +123,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
|
||||
"port",
|
||||
"dbname",
|
||||
"user",
|
||||
"password"
|
||||
"password",
|
||||
]);
|
||||
setHostname(kv.get("host") ? kv.get("host") + "" : "");
|
||||
setPort(kv.get("port") ? kv.get("port") + "" : "");
|
||||
@@ -137,6 +141,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
|
||||
/>
|
||||
}
|
||||
label="Enter DSN String"
|
||||
className={classes.formSlider}
|
||||
/>
|
||||
</Grid>
|
||||
{useDsnString ? (
|
||||
@@ -232,13 +237,13 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
|
||||
id="format"
|
||||
name="format"
|
||||
label="Format"
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
setFormat(e.target.value);
|
||||
}}
|
||||
tooltip="'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'"
|
||||
selectorOptions={[
|
||||
{ label: "Namespace", value: "namespace" },
|
||||
{ label: "Access", value: "access" }
|
||||
{ label: "Access", value: "access" },
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -279,12 +284,6 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,13 +22,17 @@ import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBo
|
||||
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
|
||||
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
|
||||
import { IElementValue } from "../types";
|
||||
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
|
||||
|
||||
interface IConfPostgresProps {
|
||||
onChange: (newValue: IElementValue[]) => void;
|
||||
classes: any;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => createStyles({});
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
|
||||
//Local States
|
||||
@@ -136,7 +140,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
|
||||
{ key: "format", value: format },
|
||||
{ key: "queue_dir", value: queueDir },
|
||||
{ key: "queue_limit", value: queueLimit },
|
||||
{ key: "comment", value: comment }
|
||||
{ key: "comment", value: comment },
|
||||
];
|
||||
|
||||
onChange(formValues);
|
||||
@@ -148,7 +152,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
|
||||
queueDir,
|
||||
queueLimit,
|
||||
comment,
|
||||
onChange
|
||||
onChange,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -162,11 +166,11 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
|
||||
sslMode,
|
||||
host,
|
||||
setConnectionString,
|
||||
configToString
|
||||
configToString,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid container className={classes.formScrollable}>
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -185,7 +189,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
|
||||
"dbname",
|
||||
"user",
|
||||
"password",
|
||||
"sslmode"
|
||||
"sslmode",
|
||||
]);
|
||||
setHostname(kv.get("host") ? kv.get("host") + "" : "");
|
||||
setPort(kv.get("port") ? kv.get("port") + "" : "");
|
||||
@@ -206,6 +210,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
|
||||
/>
|
||||
}
|
||||
label="Enter Connection String"
|
||||
className={classes.formSlider}
|
||||
/>
|
||||
</Grid>
|
||||
{useConnectionString ? (
|
||||
@@ -273,7 +278,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
|
||||
{ label: "Require", value: "require" },
|
||||
{ label: "Disable", value: "disable" },
|
||||
{ label: "Verify CA", value: "verify-ca" },
|
||||
{ label: "Verify Full", value: "verify-full" }
|
||||
{ label: "Verify Full", value: "verify-full" },
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -320,13 +325,13 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
|
||||
id="format"
|
||||
name="format"
|
||||
label="Format"
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
setFormat(e.target.value);
|
||||
}}
|
||||
tooltip="'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'"
|
||||
selectorOptions={[
|
||||
{ label: "Namespace", value: "namespace" },
|
||||
{ label: "Access", value: "access" }
|
||||
{ label: "Access", value: "access" },
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -367,12 +372,6 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,35 +15,38 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import get from "lodash/get";
|
||||
import { connect } from "react-redux";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { Button } from "@material-ui/core";
|
||||
import { Button, LinearProgress } from "@material-ui/core";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
|
||||
import api from "../../../../common/api";
|
||||
import { serverNeedsRestart } from "../../../../actions";
|
||||
import { connect } from "react-redux";
|
||||
import ConfTargetGeneric from "../ConfTargetGeneric";
|
||||
import { serverNeedsRestart } from "../../../../actions";
|
||||
import { fieldBasic } from "../../Common/FormComponents/common/styleLibrary";
|
||||
import { fieldsConfigurations, removeEmptyFields } from "../utils";
|
||||
import { IConfigurationElement, IElementValue } from "../types";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...fieldBasic,
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
strongText: {
|
||||
fontWeight: 700
|
||||
fontWeight: 700,
|
||||
},
|
||||
keyName: {
|
||||
marginLeft: 5
|
||||
marginLeft: 5,
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right"
|
||||
textAlign: "right",
|
||||
},
|
||||
logoButton: {
|
||||
height: "80px"
|
||||
}
|
||||
height: "80px",
|
||||
},
|
||||
});
|
||||
|
||||
interface IAddNotificationEndpointProps {
|
||||
@@ -59,19 +62,37 @@ const EditConfiguration = ({
|
||||
closeModalAndRefresh,
|
||||
serverNeedsRestart,
|
||||
selectedConfiguration,
|
||||
classes
|
||||
classes,
|
||||
}: IAddNotificationEndpointProps) => {
|
||||
//Local States
|
||||
const [valuesObj, setValueObj] = useState<IElementValue[]>([]);
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
const [addError, setError] = useState<string>("");
|
||||
|
||||
const [loadingConfig, setLoadingConfig] = useState<boolean>(true);
|
||||
const [errorConfig, setErrorConfig] = useState<string>("");
|
||||
const [configValues, setConfigValues] = useState<IElementValue[]>([]);
|
||||
//Effects
|
||||
useEffect(() => {
|
||||
const configId = get(selectedConfiguration, "configuration_id", false);
|
||||
|
||||
if (configId) {
|
||||
api
|
||||
.invoke("GET", `/api/v1/configs/${configId}`)
|
||||
.then((res) => {
|
||||
const keyVals = get(res, "key_values", []);
|
||||
setConfigValues(keyVals);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoadingConfig(false);
|
||||
setErrorConfig(err);
|
||||
});
|
||||
}
|
||||
setLoadingConfig(false);
|
||||
}, [selectedConfiguration]);
|
||||
|
||||
useEffect(() => {
|
||||
if (saving) {
|
||||
const payload = {
|
||||
key_values: removeEmptyFields(valuesObj)
|
||||
key_values: removeEmptyFields(valuesObj),
|
||||
};
|
||||
api
|
||||
.invoke(
|
||||
@@ -79,16 +100,16 @@ const EditConfiguration = ({
|
||||
`/api/v1/configs/${selectedConfiguration.configuration_id}`,
|
||||
payload
|
||||
)
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
setSaving(false);
|
||||
setError("");
|
||||
setErrorConfig("");
|
||||
serverNeedsRestart(true);
|
||||
|
||||
closeModalAndRefresh();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setSaving(false);
|
||||
setError(err);
|
||||
setErrorConfig(err);
|
||||
});
|
||||
}
|
||||
}, [
|
||||
@@ -96,7 +117,7 @@ const EditConfiguration = ({
|
||||
serverNeedsRestart,
|
||||
selectedConfiguration,
|
||||
valuesObj,
|
||||
closeModalAndRefresh
|
||||
closeModalAndRefresh,
|
||||
]);
|
||||
|
||||
//Fetch Actions
|
||||
@@ -106,7 +127,7 @@ const EditConfiguration = ({
|
||||
};
|
||||
|
||||
const onValueChange = useCallback(
|
||||
newValue => {
|
||||
(newValue) => {
|
||||
setValueObj(newValue);
|
||||
},
|
||||
[setValueObj]
|
||||
@@ -119,14 +140,14 @@ const EditConfiguration = ({
|
||||
title={selectedConfiguration.configuration_label}
|
||||
>
|
||||
<React.Fragment>
|
||||
{addError !== "" && (
|
||||
{errorConfig !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
{errorConfig}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
@@ -136,18 +157,23 @@ const EditConfiguration = ({
|
||||
fieldsConfigurations[selectedConfiguration.configuration_id]
|
||||
}
|
||||
onChange={onValueChange}
|
||||
defaultVals={configValues}
|
||||
/>
|
||||
<Grid item xs={3} className={classes.buttonContainer}>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
disabled={saving}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Grid>
|
||||
{loadingConfig && (
|
||||
<Grid item xs={12}>
|
||||
<LinearProgress />
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={9} />
|
||||
</form>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -31,36 +31,34 @@ export const configurationElements: IConfigurationElement[] = [
|
||||
{ configuration_id: "cache", configuration_label: "Cache Configuration" },
|
||||
{
|
||||
configuration_id: "compression",
|
||||
configuration_label: "Compression Configuration"
|
||||
configuration_label: "Compression Configuration",
|
||||
},
|
||||
{ configuration_id: "etcd", configuration_label: "Etcd Configuration" },
|
||||
{
|
||||
configuration_id: "identity_openid",
|
||||
configuration_label: "Identity Openid Configuration"
|
||||
configuration_label: "Identity Openid Configuration",
|
||||
},
|
||||
{
|
||||
configuration_id: "identity_ldap",
|
||||
configuration_label: "Identity LDAP Configuration"
|
||||
configuration_label: "Identity LDAP Configuration",
|
||||
},
|
||||
{
|
||||
configuration_id: "policy_opa",
|
||||
configuration_label: "Policy OPA Configuration"
|
||||
configuration_label: "Policy OPA Configuration",
|
||||
},
|
||||
{
|
||||
configuration_id: "kms_vault",
|
||||
configuration_label: "KMS Vault Configuration"
|
||||
configuration_label: "KMS Vault Configuration",
|
||||
},
|
||||
{ configuration_id: "kms_kes", configuration_label: "KMS KES Configuration" },
|
||||
{
|
||||
configuration_id: "logger_webhook",
|
||||
configuration_label: "Logger Webhook Configuration",
|
||||
url: "/webhook/logger"
|
||||
},
|
||||
{
|
||||
configuration_id: "audit_webhook",
|
||||
configuration_label: "Audit Webhook Configuration",
|
||||
url: "/webhook/audit"
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export const fieldsConfigurations: any = {
|
||||
@@ -70,7 +68,7 @@ export const fieldsConfigurations: any = {
|
||||
required: true,
|
||||
label: "name",
|
||||
tooltip: 'Name of the location of the server e.g. "us-west-rack2"',
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "comment",
|
||||
@@ -78,8 +76,8 @@ export const fieldsConfigurations: any = {
|
||||
label: "comment",
|
||||
tooltip: "You can add a comment to this setting",
|
||||
type: "string",
|
||||
multiline: true
|
||||
}
|
||||
multiline: true,
|
||||
},
|
||||
],
|
||||
cache: [
|
||||
{
|
||||
@@ -88,21 +86,21 @@ export const fieldsConfigurations: any = {
|
||||
label: "Drives",
|
||||
tooltip:
|
||||
'Mountpoints e.g. "/optane1" or "/optane2", you can write one per field',
|
||||
type: "csv"
|
||||
type: "csv",
|
||||
},
|
||||
{
|
||||
name: "expiry",
|
||||
required: false,
|
||||
label: "Expiry",
|
||||
tooltip: 'Cache expiry duration in days e.g. "90"',
|
||||
type: "number"
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
name: "quota",
|
||||
required: false,
|
||||
label: "Quota",
|
||||
tooltip: 'Limit cache drive usage in percentage e.g. "90"',
|
||||
type: "number"
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
name: "exclude",
|
||||
@@ -110,14 +108,28 @@ export const fieldsConfigurations: any = {
|
||||
label: "Exclude",
|
||||
tooltip:
|
||||
'Wildcard exclusion patterns e.g. "bucket/*.tmp" or "*.exe", you can write one per field',
|
||||
type: "csv"
|
||||
type: "csv",
|
||||
},
|
||||
{
|
||||
name: "after",
|
||||
required: false,
|
||||
label: "After",
|
||||
tooltip: "Minimum number of access before caching an object",
|
||||
type: "number"
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
name: "watermark_low",
|
||||
required: false,
|
||||
label: "Watermark Low",
|
||||
tooltip: "Watermark Low",
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
name: "watermark_high",
|
||||
required: false,
|
||||
label: "Watermark High",
|
||||
tooltip: "Watermark High",
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
name: "comment",
|
||||
@@ -125,23 +137,17 @@ export const fieldsConfigurations: any = {
|
||||
label: "Comment",
|
||||
tooltip: "You can add a comment to this setting",
|
||||
type: "string",
|
||||
multiline: true
|
||||
}
|
||||
multiline: true,
|
||||
},
|
||||
],
|
||||
compression: [
|
||||
{
|
||||
name: "minio_compress",
|
||||
required: true,
|
||||
label: "MinIO Compress",
|
||||
type: "on|off"
|
||||
},
|
||||
{
|
||||
name: "extensions",
|
||||
required: false,
|
||||
label: "Extensions",
|
||||
tooltip:
|
||||
'Extensions to compress e.g. ".txt",".log" or ".csv", you can write one per field',
|
||||
type: "csv"
|
||||
type: "csv",
|
||||
},
|
||||
{
|
||||
name: "mime_types",
|
||||
@@ -149,8 +155,8 @@ export const fieldsConfigurations: any = {
|
||||
label: "Mime Types",
|
||||
tooltip:
|
||||
'Mime types e.g. "text/*","application/json" or "application/xml", you can write one per field',
|
||||
type: "csv"
|
||||
}
|
||||
type: "csv",
|
||||
},
|
||||
],
|
||||
etcd: [
|
||||
{
|
||||
@@ -159,35 +165,35 @@ export const fieldsConfigurations: any = {
|
||||
label: "Endpoints",
|
||||
tooltip:
|
||||
'List of etcd endpoints e.g. "http://localhost:2379", you can write one per field',
|
||||
type: "csv"
|
||||
type: "csv",
|
||||
},
|
||||
{
|
||||
name: "path_prefix",
|
||||
required: false,
|
||||
label: "Path Prefix",
|
||||
tooltip: 'namespace prefix to isolate tenants e.g. "customer1/"',
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "coredns_path",
|
||||
required: false,
|
||||
label: "Coredns Path",
|
||||
tooltip: 'Shared bucket DNS records, default is "/skydns"',
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "client_cert",
|
||||
required: false,
|
||||
label: "Client Cert",
|
||||
tooltip: "Client cert for mTLS authentication",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "client_cert_key",
|
||||
required: false,
|
||||
label: "Client Cert Key",
|
||||
tooltip: "Client cert key for mTLS authentication",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "comment",
|
||||
@@ -195,23 +201,37 @@ export const fieldsConfigurations: any = {
|
||||
label: "Comment",
|
||||
tooltip: "You can add a comment to this setting",
|
||||
type: "string",
|
||||
multiline: true
|
||||
}
|
||||
multiline: true,
|
||||
},
|
||||
],
|
||||
identity_openid: [
|
||||
{
|
||||
name: "client_id",
|
||||
required: false,
|
||||
label: "Client ID",
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
name: "config_url",
|
||||
required: false,
|
||||
label: "Config URL",
|
||||
tooltip: "Config URL for Client ID configuration",
|
||||
type: "string"
|
||||
}
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "client_id",
|
||||
required: false,
|
||||
label: "Client ID",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "claim_name",
|
||||
required: false,
|
||||
label: "Claim Name",
|
||||
tooltip: "Claim Name",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "claim_prefix",
|
||||
required: false,
|
||||
label: "Claim Prefix",
|
||||
tooltip: "Claim Prefix",
|
||||
type: "string",
|
||||
},
|
||||
],
|
||||
identity_ldap: [
|
||||
{
|
||||
@@ -219,7 +239,7 @@ export const fieldsConfigurations: any = {
|
||||
required: true,
|
||||
label: "Server ADDR",
|
||||
tooltip: 'AD/LDAP server address e.g. "myldapserver.com:636"',
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "username_format",
|
||||
@@ -227,7 +247,7 @@ export const fieldsConfigurations: any = {
|
||||
label: "Username Format",
|
||||
tooltip:
|
||||
'List of username bind DNs e.g. "uid=%s","cn=accounts","dc=myldapserver" or "dc=com", you can write one per field',
|
||||
type: "csv"
|
||||
type: "csv",
|
||||
},
|
||||
{
|
||||
name: "username_search_filter",
|
||||
@@ -235,7 +255,7 @@ export const fieldsConfigurations: any = {
|
||||
label: "Username Search Filter",
|
||||
tooltip:
|
||||
'User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"',
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "group_search_filter",
|
||||
@@ -243,21 +263,21 @@ export const fieldsConfigurations: any = {
|
||||
label: "Group Search Filter",
|
||||
tooltip:
|
||||
'Search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))"',
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "username_search_base_dn",
|
||||
required: false,
|
||||
label: "Username Search Base DN",
|
||||
tooltip: "List of username search DNs, you can write one per field",
|
||||
type: "csv"
|
||||
type: "csv",
|
||||
},
|
||||
{
|
||||
name: "group_name_attribute",
|
||||
required: false,
|
||||
label: "Group Name Attribute",
|
||||
tooltip: 'Search attribute for group name e.g. "cn"',
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "sts_expiry",
|
||||
@@ -265,7 +285,7 @@ export const fieldsConfigurations: any = {
|
||||
label: "STS Expiry",
|
||||
tooltip:
|
||||
'temporary credentials validity duration in s,m,h,d. Default is "1h"',
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "tls_skip_verify",
|
||||
@@ -273,7 +293,7 @@ export const fieldsConfigurations: any = {
|
||||
label: "TLS Skip Verify",
|
||||
tooltip:
|
||||
'Trust server TLS without verification, defaults to "off" (verify)',
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "server_insecure",
|
||||
@@ -281,7 +301,7 @@ export const fieldsConfigurations: any = {
|
||||
label: "Server Insecure",
|
||||
tooltip:
|
||||
'Allow plain text connection to AD/LDAP server, defaults to "off"',
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "comment",
|
||||
@@ -289,61 +309,59 @@ export const fieldsConfigurations: any = {
|
||||
label: "Comment",
|
||||
tooltip: "Optionally add a comment to this setting",
|
||||
type: "string",
|
||||
multiline: true
|
||||
}
|
||||
multiline: true,
|
||||
},
|
||||
],
|
||||
policy_opa: [
|
||||
{
|
||||
name: "opa_url",
|
||||
name: "url",
|
||||
required: true,
|
||||
label: "OPA URL",
|
||||
type: "string"
|
||||
}
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "auth_token",
|
||||
required: true,
|
||||
label: "Auth Token",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "policy_opa",
|
||||
required: true,
|
||||
label: "Policy OPA",
|
||||
type: "string",
|
||||
},
|
||||
],
|
||||
kms_vault: [],
|
||||
kms_kes: [],
|
||||
logger_webhook: [
|
||||
{
|
||||
name: "name",
|
||||
name: "endpoint",
|
||||
required: true,
|
||||
label: "Name",
|
||||
tooltip: "Name of the webhook",
|
||||
type: "string"
|
||||
label: "Endpoint",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "auth_token",
|
||||
required: true,
|
||||
label: "Auth Token",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "endpoint",
|
||||
required: true,
|
||||
label: "Endpoint",
|
||||
type: "string"
|
||||
}
|
||||
],
|
||||
audit_webhook: [
|
||||
{
|
||||
name: "name",
|
||||
name: "endpoint",
|
||||
required: true,
|
||||
label: "Name",
|
||||
tooltip: "Name of the webhook",
|
||||
type: "string"
|
||||
label: "Endpoint",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "auth_token",
|
||||
required: true,
|
||||
label: "Auth Token",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "endpoint",
|
||||
required: true,
|
||||
label: "Endpoint",
|
||||
type: "string"
|
||||
}
|
||||
]
|
||||
],
|
||||
};
|
||||
|
||||
const commonFields = [
|
||||
@@ -353,7 +371,7 @@ const commonFields = [
|
||||
required: true,
|
||||
|
||||
tooltip: "staging dir for undelivered messages e.g. '/home/events'",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "queue-limit",
|
||||
@@ -361,15 +379,15 @@ const commonFields = [
|
||||
required: false,
|
||||
|
||||
tooltip: "maximum limit for undelivered messages, defaults to '10000'",
|
||||
type: "number"
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
name: "comment",
|
||||
label: "Comment",
|
||||
required: false,
|
||||
type: "string",
|
||||
multiline: true
|
||||
}
|
||||
multiline: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const notificationEndpointsFields: any = {
|
||||
@@ -380,77 +398,77 @@ export const notificationEndpointsFields: any = {
|
||||
required: true,
|
||||
|
||||
tooltip: "comma separated list of Kafka broker addresses",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "topic",
|
||||
label: "Topic",
|
||||
tooltip: "Kafka topic used for bucket notifications",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "sasl_username",
|
||||
label: "SASL Username",
|
||||
tooltip: "username for SASL/PLAIN or SASL/SCRAM authentication",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "sasl_password",
|
||||
label: "SASL Password",
|
||||
tooltip: "password for SASL/PLAIN or SASL/SCRAM authentication",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "sasl_mechanism",
|
||||
label: "SASL Mechanism",
|
||||
tooltip: "sasl authentication mechanism, default 'PLAIN'",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "tls_client_auth",
|
||||
label: "TLS Client Auth",
|
||||
tooltip:
|
||||
"clientAuth determines the Kafka server's policy for TLS client auth",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "sasl",
|
||||
label: "SASL",
|
||||
tooltip: "set to 'on' to enable SASL authentication",
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "tls",
|
||||
label: "TLS",
|
||||
tooltip: "set to 'on' to enable TLS",
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "tls_skip_verify",
|
||||
label: "TLS skip verify",
|
||||
tooltip:
|
||||
'trust server TLS without verification, defaults to "on" (verify)',
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "client_tls_cert",
|
||||
label: "client TLS cert",
|
||||
tooltip: "path to client certificate for mTLS auth",
|
||||
type: "path"
|
||||
type: "path",
|
||||
},
|
||||
{
|
||||
name: "client_tls_key",
|
||||
label: "client TLS key",
|
||||
tooltip: "path to client key for mTLS auth",
|
||||
type: "path"
|
||||
type: "path",
|
||||
},
|
||||
{
|
||||
name: "version",
|
||||
label: "Version",
|
||||
tooltip: "specify the version of the Kafka cluster e.g '2.2.0'",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
...commonFields
|
||||
...commonFields,
|
||||
],
|
||||
[notifyAmqp]: [
|
||||
{
|
||||
@@ -459,68 +477,68 @@ export const notificationEndpointsFields: any = {
|
||||
label: "url",
|
||||
tooltip:
|
||||
"AMQP server endpoint e.g. `amqp://myuser:mypassword@localhost:5672`",
|
||||
type: "url"
|
||||
type: "url",
|
||||
},
|
||||
{
|
||||
name: "exchange",
|
||||
label: "exchange",
|
||||
tooltip: "name of the AMQP exchange",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "exchange_type",
|
||||
label: "exchange_type",
|
||||
tooltip: "AMQP exchange type",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "routing_key",
|
||||
label: "routing_key",
|
||||
tooltip: "routing key for publishing",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "mandatory",
|
||||
label: "mandatory",
|
||||
tooltip:
|
||||
"quietly ignore undelivered messages when set to 'off', default is 'on'",
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "durable",
|
||||
label: "durable",
|
||||
tooltip:
|
||||
"persist queue across broker restarts when set to 'on', default is 'off'",
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "no_wait",
|
||||
label: "no_wait",
|
||||
tooltip:
|
||||
"non-blocking message delivery when set to 'on', default is 'off'",
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "internal",
|
||||
label: "internal",
|
||||
tooltip:
|
||||
"set to 'on' for exchange to be not used directly by publishers, but only when bound to other exchanges",
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "auto_deleted",
|
||||
label: "auto_deleted",
|
||||
tooltip:
|
||||
"auto delete queue when set to 'on', when there are no consumers",
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "delivery_mode",
|
||||
label: "delivery_mode",
|
||||
tooltip: "set to '1' for non-persistent or '2' for persistent queue",
|
||||
type: "number"
|
||||
type: "number",
|
||||
},
|
||||
...commonFields
|
||||
...commonFields,
|
||||
],
|
||||
[notifyRedis]: [
|
||||
{
|
||||
@@ -528,22 +546,22 @@ export const notificationEndpointsFields: any = {
|
||||
required: true,
|
||||
label: "address",
|
||||
tooltip: "Redis server's address. For example: `localhost:6379`",
|
||||
type: "address"
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
name: "key",
|
||||
required: true,
|
||||
label: "key",
|
||||
tooltip: "Redis key to store/update events, key is auto-created",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "password",
|
||||
label: "password",
|
||||
tooltip: "Redis server password",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
...commonFields
|
||||
...commonFields,
|
||||
],
|
||||
[notifyMqtt]: [
|
||||
{
|
||||
@@ -551,46 +569,46 @@ export const notificationEndpointsFields: any = {
|
||||
required: true,
|
||||
label: "broker",
|
||||
tooltip: "MQTT server endpoint e.g. `tcp://localhost:1883`",
|
||||
type: "uri"
|
||||
type: "uri",
|
||||
},
|
||||
{
|
||||
name: "topic",
|
||||
required: true,
|
||||
label: "topic",
|
||||
tooltip: "name of the MQTT topic to publish",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
label: "username",
|
||||
tooltip: "MQTT username",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "password",
|
||||
label: "password",
|
||||
tooltip: "MQTT password",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "qos",
|
||||
label: "qos",
|
||||
tooltip: "set the quality of service priority, defaults to '0'",
|
||||
type: "number"
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
name: "keep_alive_interval",
|
||||
label: "keep_alive_interval",
|
||||
tooltip: "keep-alive interval for MQTT connections in s,m,h,d",
|
||||
type: "duration"
|
||||
type: "duration",
|
||||
},
|
||||
{
|
||||
name: "reconnect_interval",
|
||||
label: "reconnect_interval",
|
||||
tooltip: "reconnect interval for MQTT connections in s,m,h,d",
|
||||
type: "duration"
|
||||
type: "duration",
|
||||
},
|
||||
...commonFields
|
||||
...commonFields,
|
||||
],
|
||||
[notifyNats]: [
|
||||
{
|
||||
@@ -598,95 +616,95 @@ export const notificationEndpointsFields: any = {
|
||||
required: true,
|
||||
label: "address",
|
||||
tooltip: "NATS server address e.g. '0.0.0.0:4222'",
|
||||
type: "address"
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
name: "subject",
|
||||
required: true,
|
||||
label: "subject",
|
||||
tooltip: "NATS subscription subject",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
label: "username",
|
||||
tooltip: "NATS username",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "password",
|
||||
label: "password",
|
||||
tooltip: "NATS password",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "token",
|
||||
label: "token",
|
||||
tooltip: "NATS token",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "tls",
|
||||
label: "tls",
|
||||
tooltip: "set to 'on' to enable TLS",
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "tls_skip_verify",
|
||||
label: "tls_skip_verify",
|
||||
tooltip:
|
||||
'trust server TLS without verification, defaults to "on" (verify)',
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "ping_interval",
|
||||
label: "ping_interval",
|
||||
tooltip: "client ping commands interval in s,m,h,d. Disabled by default",
|
||||
type: "duration"
|
||||
type: "duration",
|
||||
},
|
||||
{
|
||||
name: "streaming",
|
||||
label: "streaming",
|
||||
tooltip: "set to 'on', to use streaming NATS server",
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "streaming_async",
|
||||
label: "streaming_async",
|
||||
tooltip: "set to 'on', to enable asynchronous publish",
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "streaming_max_pub_acks_in_flight",
|
||||
label: "streaming_max_pub_acks_in_flight",
|
||||
tooltip: "number of messages to publish without waiting for ACKs",
|
||||
type: "number"
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
name: "streaming_cluster_id",
|
||||
label: "streaming_cluster_id",
|
||||
tooltip: "unique ID for NATS streaming cluster",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "cert_authority",
|
||||
label: "cert_authority",
|
||||
tooltip: "path to certificate chain of the target NATS server",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "client_cert",
|
||||
label: "client_cert",
|
||||
tooltip: "client cert for NATS mTLS auth",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "client_key",
|
||||
label: "client_key",
|
||||
tooltip: "client cert key for NATS mTLS auth",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
...commonFields
|
||||
...commonFields,
|
||||
],
|
||||
[notifyElasticsearch]: [
|
||||
{
|
||||
@@ -695,7 +713,7 @@ export const notificationEndpointsFields: any = {
|
||||
label: "url",
|
||||
tooltip:
|
||||
"Elasticsearch server's address, with optional authentication info",
|
||||
type: "url"
|
||||
type: "url",
|
||||
},
|
||||
{
|
||||
name: "index",
|
||||
@@ -703,7 +721,7 @@ export const notificationEndpointsFields: any = {
|
||||
label: "index",
|
||||
tooltip:
|
||||
"Elasticsearch index to store/update events, index is auto-created",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "format",
|
||||
@@ -711,9 +729,9 @@ export const notificationEndpointsFields: any = {
|
||||
label: "format",
|
||||
tooltip:
|
||||
"'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'",
|
||||
type: "enum"
|
||||
type: "enum",
|
||||
},
|
||||
...commonFields
|
||||
...commonFields,
|
||||
],
|
||||
[notifyWebhooks]: [
|
||||
{
|
||||
@@ -722,15 +740,15 @@ export const notificationEndpointsFields: any = {
|
||||
label: "endpoint",
|
||||
tooltip:
|
||||
"webhook server endpoint e.g. http://localhost:8080/minio/events",
|
||||
type: "url"
|
||||
type: "url",
|
||||
},
|
||||
{
|
||||
name: "auth_token",
|
||||
label: "auth_token",
|
||||
tooltip: "opaque string or JWT authorization token",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
...commonFields
|
||||
...commonFields,
|
||||
],
|
||||
[notifyNsq]: [
|
||||
{
|
||||
@@ -738,34 +756,34 @@ export const notificationEndpointsFields: any = {
|
||||
required: true,
|
||||
label: "nsqd_address",
|
||||
tooltip: "NSQ server address e.g. '127.0.0.1:4150'",
|
||||
type: "address"
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
name: "topic",
|
||||
required: true,
|
||||
label: "topic",
|
||||
tooltip: "NSQ topic",
|
||||
type: "string"
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "tls",
|
||||
label: "tls",
|
||||
tooltip: "set to 'on' to enable TLS",
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
{
|
||||
name: "tls_skip_verify",
|
||||
label: "tls_skip_verify",
|
||||
tooltip:
|
||||
'trust server TLS without verification, defaults to "on" (verify)',
|
||||
type: "on|off"
|
||||
type: "on|off",
|
||||
},
|
||||
...commonFields
|
||||
]
|
||||
...commonFields,
|
||||
],
|
||||
};
|
||||
|
||||
export const removeEmptyFields = (formFields: IElementValue[]) => {
|
||||
const nonEmptyFields = formFields.filter(field => field.value !== "");
|
||||
const nonEmptyFields = formFields.filter((field) => field.value !== "");
|
||||
|
||||
return nonEmptyFields;
|
||||
};
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
createStyles,
|
||||
StyledProps,
|
||||
Theme,
|
||||
withStyles
|
||||
withStyles,
|
||||
} from "@material-ui/core/styles";
|
||||
import CssBaseline from "@material-ui/core/CssBaseline";
|
||||
import Drawer from "@material-ui/core/Drawer";
|
||||
@@ -28,7 +28,6 @@ import Box from "@material-ui/core/Box";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Container from "@material-ui/core/Container";
|
||||
import Link from "@material-ui/core/Link";
|
||||
|
||||
import history from "../../history";
|
||||
import {
|
||||
Redirect,
|
||||
@@ -36,14 +35,14 @@ import {
|
||||
RouteComponentProps,
|
||||
Router,
|
||||
Switch,
|
||||
withRouter
|
||||
withRouter,
|
||||
} from "react-router-dom";
|
||||
import { connect } from "react-redux";
|
||||
import { AppState } from "../../store";
|
||||
import {
|
||||
serverIsLoading,
|
||||
serverNeedsRestart,
|
||||
setMenuOpen
|
||||
setMenuOpen,
|
||||
} from "../../actions";
|
||||
import { ThemedComponentProps } from "@material-ui/core/styles/withTheme";
|
||||
import Buckets from "./Buckets/Buckets";
|
||||
@@ -62,6 +61,10 @@ import ConfigurationsList from "./Configurations/ConfigurationPanels/Configurati
|
||||
import { Button, LinearProgress } from "@material-ui/core";
|
||||
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
|
||||
import Trace from "./Trace/Trace";
|
||||
import Logs from "./Logs/Logs";
|
||||
import Watch from "./Watch/Watch";
|
||||
import { ISessionResponse } from "./types";
|
||||
import { saveSessionResponse } from "./actions";
|
||||
|
||||
function Copyright() {
|
||||
return (
|
||||
@@ -81,43 +84,43 @@ const drawerWidth = 254;
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
display: "flex"
|
||||
display: "flex",
|
||||
},
|
||||
toolbar: {
|
||||
background: theme.palette.background.default,
|
||||
color: "black",
|
||||
paddingRight: 24 // keep right padding when drawer closed
|
||||
paddingRight: 24, // keep right padding when drawer closed
|
||||
},
|
||||
toolbarIcon: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
padding: "0 8px",
|
||||
...theme.mixins.toolbar
|
||||
...theme.mixins.toolbar,
|
||||
},
|
||||
appBar: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
transition: theme.transitions.create(["width", "margin"], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen
|
||||
})
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
},
|
||||
appBarShift: {
|
||||
marginLeft: drawerWidth,
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
transition: theme.transitions.create(["width", "margin"], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen
|
||||
})
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
menuButton: {
|
||||
marginRight: 36
|
||||
marginRight: 36,
|
||||
},
|
||||
menuButtonHidden: {
|
||||
display: "none"
|
||||
display: "none",
|
||||
},
|
||||
title: {
|
||||
flexGrow: 1
|
||||
flexGrow: 1,
|
||||
},
|
||||
drawerPaper: {
|
||||
position: "relative",
|
||||
@@ -125,40 +128,40 @@ const styles = (theme: Theme) =>
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen
|
||||
})
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
drawerPaperClose: {
|
||||
overflowX: "hidden",
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
width: theme.spacing(7),
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
width: theme.spacing(9)
|
||||
}
|
||||
width: theme.spacing(9),
|
||||
},
|
||||
},
|
||||
appBarSpacer: {
|
||||
height: "5px"
|
||||
height: "5px",
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
height: "100vh",
|
||||
overflow: "auto"
|
||||
overflow: "auto",
|
||||
},
|
||||
container: {
|
||||
paddingTop: theme.spacing(4),
|
||||
paddingBottom: theme.spacing(4)
|
||||
paddingBottom: theme.spacing(4),
|
||||
},
|
||||
paper: {
|
||||
padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column"
|
||||
flexDirection: "column",
|
||||
},
|
||||
fixedHeight: {
|
||||
minHeight: 240
|
||||
minHeight: 240,
|
||||
},
|
||||
warningBar: {
|
||||
background: theme.palette.primary.main,
|
||||
@@ -166,22 +169,10 @@ const styles = (theme: Theme) =>
|
||||
heigh: "60px",
|
||||
widht: "100%",
|
||||
lineHeight: "60px",
|
||||
textAlign: "center"
|
||||
}
|
||||
textAlign: "center",
|
||||
},
|
||||
});
|
||||
|
||||
const mapState = (state: AppState) => ({
|
||||
open: state.system.sidebarOpen,
|
||||
needsRestart: state.system.serverNeedsRestart,
|
||||
isServerLoading: state.system.serverIsLoading
|
||||
});
|
||||
|
||||
const connector = connect(mapState, {
|
||||
setMenuOpen,
|
||||
serverNeedsRestart,
|
||||
serverIsLoading
|
||||
});
|
||||
|
||||
interface IConsoleProps {
|
||||
open: boolean;
|
||||
needsRestart: boolean;
|
||||
@@ -191,141 +182,206 @@ interface IConsoleProps {
|
||||
setMenuOpen: typeof setMenuOpen;
|
||||
serverNeedsRestart: typeof serverNeedsRestart;
|
||||
serverIsLoading: typeof serverIsLoading;
|
||||
saveSessionResponse: typeof saveSessionResponse;
|
||||
session: ISessionResponse;
|
||||
}
|
||||
|
||||
class Console extends React.Component<
|
||||
IConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps
|
||||
> {
|
||||
componentDidMount(): void {
|
||||
const Console = ({
|
||||
classes,
|
||||
open,
|
||||
needsRestart,
|
||||
isServerLoading,
|
||||
serverNeedsRestart,
|
||||
serverIsLoading,
|
||||
saveSessionResponse,
|
||||
session,
|
||||
}: IConsoleProps) => {
|
||||
useEffect(() => {
|
||||
api
|
||||
.invoke("GET", `/api/v1/session`)
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
.then((res) => {
|
||||
saveSessionResponse(res);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
storage.removeItem("token");
|
||||
history.push("/");
|
||||
});
|
||||
}
|
||||
}, [saveSessionResponse]);
|
||||
|
||||
restartServer() {
|
||||
this.props.serverIsLoading(true);
|
||||
const restartServer = () => {
|
||||
serverIsLoading(true);
|
||||
api
|
||||
.invoke("POST", "/api/v1/service/restart", {})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
console.log("success restarting service");
|
||||
console.log(res);
|
||||
this.props.serverIsLoading(false);
|
||||
this.props.serverNeedsRestart(false);
|
||||
serverIsLoading(false);
|
||||
serverNeedsRestart(false);
|
||||
})
|
||||
.catch(err => {
|
||||
this.props.serverIsLoading(false);
|
||||
.catch((err) => {
|
||||
serverIsLoading(false);
|
||||
console.log("failure restarting service");
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, open, needsRestart, isServerLoading } = this.props;
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<CssBaseline />
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
classes={{
|
||||
paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose)
|
||||
}}
|
||||
open={open}
|
||||
>
|
||||
{/*<div className={classes.toolbarIcon}>*/}
|
||||
{/* <IconButton*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* this.props.setMenuOpen(false);*/}
|
||||
{/* }}*/}
|
||||
{/* >*/}
|
||||
{/* <ChevronLeftIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/*</div>*/}
|
||||
{/*<Divider />*/}
|
||||
const allowedPages = session.pages.reduce(
|
||||
(result: any, item: any, index: any) => {
|
||||
result[item] = true;
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
);
|
||||
const routes = [
|
||||
{
|
||||
component: Dashboard,
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
component: Buckets,
|
||||
path: "/buckets",
|
||||
},
|
||||
{
|
||||
component: Buckets,
|
||||
path: "/buckets/:bucketName",
|
||||
},
|
||||
{
|
||||
component: Watch,
|
||||
path: "/watch",
|
||||
},
|
||||
{
|
||||
component: Users,
|
||||
path: "/users",
|
||||
},
|
||||
{
|
||||
component: Groups,
|
||||
path: "/groups",
|
||||
},
|
||||
{
|
||||
component: Policies,
|
||||
path: "/policies",
|
||||
},
|
||||
{
|
||||
component: Trace,
|
||||
path: "/trace",
|
||||
},
|
||||
{
|
||||
component: Logs,
|
||||
path: "/logs",
|
||||
},
|
||||
{
|
||||
component: ListNotificationEndpoints,
|
||||
path: "/notification-endpoints",
|
||||
},
|
||||
{
|
||||
component: ConfigurationsList,
|
||||
path: "/configurations-list",
|
||||
},
|
||||
{
|
||||
component: Permissions,
|
||||
path: "/permissions",
|
||||
},
|
||||
{
|
||||
component: ServiceAccounts,
|
||||
path: "/service-accounts",
|
||||
},
|
||||
{
|
||||
component: WebhookPanel,
|
||||
path: "/webhook/logger",
|
||||
},
|
||||
{
|
||||
component: WebhookPanel,
|
||||
path: "/webhook/audit",
|
||||
},
|
||||
];
|
||||
const allowedRoutes = routes.filter((route: any) => allowedPages[route.path]);
|
||||
|
||||
<Menu />
|
||||
</Drawer>
|
||||
return (
|
||||
<React.Fragment>
|
||||
{session.status == "ok" ? (
|
||||
<div className={classes.root}>
|
||||
<CssBaseline />
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
classes={{
|
||||
paper: clsx(
|
||||
classes.drawerPaper,
|
||||
!open && classes.drawerPaperClose
|
||||
),
|
||||
}}
|
||||
open={open}
|
||||
>
|
||||
<Menu pages={session.pages} />
|
||||
</Drawer>
|
||||
|
||||
<main className={classes.content}>
|
||||
{needsRestart && (
|
||||
<div className={classes.warningBar}>
|
||||
{isServerLoading ? (
|
||||
<React.Fragment>
|
||||
The server is restarting.
|
||||
<LinearProgress />
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
The instance needs to be restarted for configuration changes
|
||||
to take effect.{" "}
|
||||
<Button
|
||||
color="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
this.restartServer();
|
||||
}}
|
||||
>
|
||||
Restart
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.appBarSpacer} />
|
||||
<Container maxWidth="lg" className={classes.container}>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path="/buckets" component={Buckets} />
|
||||
<Route exact path="/permissions" component={Permissions} />
|
||||
<Route exact path="/policies" component={Policies} />
|
||||
<Route
|
||||
exact
|
||||
path="/service_accounts"
|
||||
component={ServiceAccounts}
|
||||
/>
|
||||
<Route exact path="/users" component={Users} />
|
||||
<Route exact path="/dashboard" component={Dashboard} />
|
||||
<Route exct path="/groups" component={Groups} />
|
||||
<Route
|
||||
exact
|
||||
path="/notification-endpoints"
|
||||
component={ListNotificationEndpoints}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/configurations-list"
|
||||
component={ConfigurationsList}
|
||||
/>
|
||||
<Route exact path="/webhook/logger" component={WebhookPanel} />
|
||||
<Route exact path="/webhook/audit" component={WebhookPanel} />
|
||||
<Route exct path="/trace" component={Trace} />
|
||||
<Route exact path="/">
|
||||
<Redirect to="/dashboard" />
|
||||
</Route>
|
||||
<Route component={NotFoundPage} />
|
||||
</Switch>
|
||||
</Router>
|
||||
<main className={classes.content}>
|
||||
{needsRestart && (
|
||||
<div className={classes.warningBar}>
|
||||
{isServerLoading ? (
|
||||
<React.Fragment>
|
||||
The server is restarting.
|
||||
<LinearProgress />
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
The instance needs to be restarted for configuration changes
|
||||
to take effect.{" "}
|
||||
<Button
|
||||
color="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
restartServer();
|
||||
}}
|
||||
>
|
||||
Restart
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.appBarSpacer} />
|
||||
<Container maxWidth="lg" className={classes.container}>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
{allowedRoutes.map((route: any) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
exact
|
||||
path={route.path}
|
||||
component={route.component}
|
||||
/>
|
||||
))}
|
||||
{allowedRoutes.length > 0 ? (
|
||||
<Route exact path="/">
|
||||
<Redirect to={allowedRoutes[0].path} />
|
||||
</Route>
|
||||
) : null}
|
||||
</Switch>
|
||||
</Router>
|
||||
|
||||
<Box pt={4}>
|
||||
<Copyright />
|
||||
</Box>
|
||||
</Container>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
<Box pt={4}>
|
||||
<Copyright />
|
||||
</Box>
|
||||
</Container>
|
||||
</main>
|
||||
</div>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
// );
|
||||
const mapState = (state: AppState) => ({
|
||||
open: state.system.sidebarOpen,
|
||||
needsRestart: state.system.serverNeedsRestart,
|
||||
isServerLoading: state.system.serverIsLoading,
|
||||
session: state.console.session,
|
||||
});
|
||||
|
||||
export default withRouter(connector(withStyles(styles)(Console)));
|
||||
// export default withStyles(styles)(connector(Console));
|
||||
// export default compose(
|
||||
// withStyles(styles),
|
||||
// connector
|
||||
// )(withRouter(Console))
|
||||
const connector = connect(mapState, {
|
||||
setMenuOpen,
|
||||
serverNeedsRestart,
|
||||
serverIsLoading,
|
||||
saveSessionResponse,
|
||||
});
|
||||
|
||||
export default connector(withStyles(styles)(Console));
|
||||
|
||||
@@ -26,86 +26,87 @@ import ViewHeadlineIcon from "@material-ui/icons/ViewHeadline";
|
||||
import { Usage } from "./types";
|
||||
import api from "../../../common/api";
|
||||
import { niceBytes } from "../../../common/utils";
|
||||
import { LinearProgress } from "@material-ui/core";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
display: "flex"
|
||||
display: "flex",
|
||||
},
|
||||
toolbar: {
|
||||
paddingRight: 24 // keep right padding when drawer closed
|
||||
paddingRight: 24, // keep right padding when drawer closed
|
||||
},
|
||||
toolbarIcon: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
padding: "0 8px",
|
||||
...theme.mixins.toolbar
|
||||
...theme.mixins.toolbar,
|
||||
},
|
||||
appBar: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
transition: theme.transitions.create(["width", "margin"], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen
|
||||
})
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
},
|
||||
|
||||
menuButton: {
|
||||
marginRight: 36
|
||||
marginRight: 36,
|
||||
},
|
||||
menuButtonHidden: {
|
||||
display: "none"
|
||||
display: "none",
|
||||
},
|
||||
title: {
|
||||
flexGrow: 1
|
||||
flexGrow: 1,
|
||||
},
|
||||
drawerPaperClose: {
|
||||
overflowX: "hidden",
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
width: theme.spacing(7),
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
width: theme.spacing(9)
|
||||
}
|
||||
width: theme.spacing(9),
|
||||
},
|
||||
},
|
||||
appBarSpacer: theme.mixins.toolbar,
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
height: "100vh",
|
||||
overflow: "auto"
|
||||
overflow: "auto",
|
||||
},
|
||||
container: {
|
||||
paddingBottom: theme.spacing(4),
|
||||
"& h6": {
|
||||
color: "#777777",
|
||||
fontSize: 14
|
||||
fontSize: 14,
|
||||
},
|
||||
"& p": {
|
||||
"& span": {
|
||||
fontSize: 16
|
||||
}
|
||||
}
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column"
|
||||
flexDirection: "column",
|
||||
},
|
||||
fixedHeight: {
|
||||
minHeight: 240
|
||||
minHeight: 240,
|
||||
},
|
||||
consumptionValue: {
|
||||
color: "#000000",
|
||||
fontSize: "60px",
|
||||
fontWeight: "bold"
|
||||
fontWeight: "bold",
|
||||
},
|
||||
icon: {
|
||||
marginRight: 10,
|
||||
color: "#777777"
|
||||
}
|
||||
color: "#777777",
|
||||
},
|
||||
});
|
||||
|
||||
interface IDashboardProps {
|
||||
@@ -115,7 +116,7 @@ interface IDashboardProps {
|
||||
const Dashboard = ({ classes }: IDashboardProps) => {
|
||||
const fixedHeightPaper = clsx(classes.paper, classes.fixedHeight);
|
||||
const [usage, setUsage] = useState<Usage | null>(null);
|
||||
const [loading, isLoading] = useState<boolean>(true);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -130,11 +131,11 @@ const Dashboard = ({ classes }: IDashboardProps) => {
|
||||
.then((res: Usage) => {
|
||||
setUsage(res);
|
||||
setError("");
|
||||
isLoading(false);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setError(err);
|
||||
isLoading(false);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
const prettyUsage = (usage: string | undefined) => {
|
||||
@@ -143,7 +144,6 @@ const Dashboard = ({ classes }: IDashboardProps) => {
|
||||
}
|
||||
return niceBytes(usage);
|
||||
};
|
||||
const units = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
|
||||
const prettyNumber = (usage: number | undefined) => {
|
||||
if (usage === undefined) {
|
||||
@@ -161,51 +161,59 @@ const Dashboard = ({ classes }: IDashboardProps) => {
|
||||
<Typography variant="h2">MinIO Console</Typography>
|
||||
</Grid>
|
||||
{error !== "" && <Grid container>{error}</Grid>}
|
||||
<Grid item xs={12} md={4} lg={4}>
|
||||
<Paper className={fixedHeightPaper}>
|
||||
<Grid container direction="row" alignItems="center">
|
||||
<Grid item className={classes.icon}>
|
||||
<ViewHeadlineIcon />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="h6">Total Buckets</Typography>
|
||||
</Grid>
|
||||
{loading ? (
|
||||
<Grid item xs={12} md={12} lg={12}>
|
||||
<LinearProgress />
|
||||
</Grid>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12} md={4} lg={4}>
|
||||
<Paper className={fixedHeightPaper}>
|
||||
<Grid container direction="row" alignItems="center">
|
||||
<Grid item className={classes.icon}>
|
||||
<ViewHeadlineIcon />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="h6">Total Buckets</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography className={classes.consumptionValue}>
|
||||
{usage ? prettyNumber(usage.buckets) : 0}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Typography className={classes.consumptionValue}>
|
||||
{usage ? prettyNumber(usage.buckets) : 0}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4} lg={4}>
|
||||
<Paper className={fixedHeightPaper}>
|
||||
<Grid container direction="row" alignItems="center">
|
||||
<Grid item className={classes.icon}>
|
||||
<NetworkCheckIcon />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="h6"> Total Objects</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4} lg={4}>
|
||||
<Paper className={fixedHeightPaper}>
|
||||
<Grid container direction="row" alignItems="center">
|
||||
<Grid item className={classes.icon}>
|
||||
<NetworkCheckIcon />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="h6"> Total Objects</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography className={classes.consumptionValue}>
|
||||
{usage ? prettyNumber(usage.objects) : 0}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Typography className={classes.consumptionValue}>
|
||||
{usage ? prettyNumber(usage.objects) : 0}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4} lg={4}>
|
||||
<Paper className={fixedHeightPaper}>
|
||||
<Grid container direction="row" alignItems="center">
|
||||
<Grid item className={classes.icon}>
|
||||
<PieChartIcon />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="h6">Usage</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4} lg={4}>
|
||||
<Paper className={fixedHeightPaper}>
|
||||
<Grid container direction="row" alignItems="center">
|
||||
<Grid item className={classes.icon}>
|
||||
<PieChartIcon />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="h6">Usage</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography className={classes.consumptionValue}>
|
||||
{usage ? prettyUsage(usage.usage + "") : 0}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Typography className={classes.consumptionValue}>
|
||||
{usage ? prettyUsage(usage.usage + "") : 0}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { Button, LinearProgress } from "@material-ui/core";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
|
||||
import api from "../../../common/api";
|
||||
import UsersSelectors from "./UsersSelectors";
|
||||
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
|
||||
@@ -41,24 +42,25 @@ interface MainGroupProps {
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
strongText: {
|
||||
fontWeight: 700
|
||||
fontWeight: 700,
|
||||
},
|
||||
keyName: {
|
||||
marginLeft: 5
|
||||
marginLeft: 5,
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right"
|
||||
}
|
||||
textAlign: "right",
|
||||
},
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
const AddGroup = ({
|
||||
open,
|
||||
selectedGroup,
|
||||
closeModalAndRefresh,
|
||||
classes
|
||||
classes,
|
||||
}: IGroupProps) => {
|
||||
//Local States
|
||||
const [groupName, setGroupName] = useState<string>("");
|
||||
@@ -86,14 +88,14 @@ const AddGroup = ({
|
||||
.invoke("PUT", `/api/v1/groups/${groupName}`, {
|
||||
group: groupName,
|
||||
members: selectedUsers,
|
||||
status: groupEnabled
|
||||
status: groupEnabled,
|
||||
})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
isSaving(false);
|
||||
setError("");
|
||||
closeModalAndRefresh();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
isSaving(false);
|
||||
setError(err);
|
||||
});
|
||||
@@ -101,14 +103,14 @@ const AddGroup = ({
|
||||
api
|
||||
.invoke("POST", "/api/v1/groups", {
|
||||
group: groupName,
|
||||
members: selectedUsers
|
||||
members: selectedUsers,
|
||||
})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
isSaving(false);
|
||||
setError("");
|
||||
closeModalAndRefresh();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
isSaving(false);
|
||||
setError(err);
|
||||
});
|
||||
@@ -122,7 +124,7 @@ const AddGroup = ({
|
||||
selectedUsers,
|
||||
groupEnabled,
|
||||
selectedGroup,
|
||||
closeModalAndRefresh
|
||||
closeModalAndRefresh,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -135,7 +137,7 @@ const AddGroup = ({
|
||||
setGroupName(res.name);
|
||||
setSelectedUsers(res.members);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setError(err);
|
||||
isLoadingGroup(false);
|
||||
});
|
||||
@@ -159,62 +161,61 @@ const AddGroup = ({
|
||||
>
|
||||
<form noValidate autoComplete="off" onSubmit={setSaving}>
|
||||
<Grid container>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{selectedGroup !== null ? (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12}>
|
||||
<RadioGroupSelector
|
||||
currentSelection={groupEnabled}
|
||||
id="group-status"
|
||||
name="group-status"
|
||||
label="Status"
|
||||
onChange={e => {
|
||||
setGroupEnabled(e.target.value);
|
||||
}}
|
||||
selectorOptions={[
|
||||
{ label: "Enabled", value: "enabled" },
|
||||
{ label: "Disabled", value: "disabled" }
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="group-name"
|
||||
name="group-name"
|
||||
label="Name"
|
||||
value={groupName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setGroupName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<UsersSelectors
|
||||
selectedUsers={selectedUsers}
|
||||
setSelectedUsers={setSelectedUsers}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
{selectedGroup !== null ? (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12}>
|
||||
<RadioGroupSelector
|
||||
currentSelection={groupEnabled}
|
||||
id="group-status"
|
||||
name="group-status"
|
||||
label="Status"
|
||||
onChange={(e) => {
|
||||
setGroupEnabled(e.target.value);
|
||||
}}
|
||||
selectorOptions={[
|
||||
{ label: "Enabled", value: "enabled" },
|
||||
{ label: "Disabled", value: "disabled" },
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="group-name"
|
||||
name="group-name"
|
||||
label="Name"
|
||||
value={groupName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setGroupName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<UsersSelectors
|
||||
selectedUsers={selectedUsers}
|
||||
setSelectedUsers={setSelectedUsers}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
|
||||
@@ -26,7 +26,7 @@ import { CreateIcon } from "../../../icons";
|
||||
import api from "../../../common/api";
|
||||
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
|
||||
import { GroupsList } from "./types";
|
||||
import { groupsSort } from "../../../utils/sortFunctions";
|
||||
import { stringSort } from "../../../utils/sortFunctions";
|
||||
import AddGroup from "../Groups/AddGroup";
|
||||
import DeleteGroup from "./DeleteGroup";
|
||||
import TableWrapper from "../Common/TableWrapper/TableWrapper";
|
||||
@@ -39,50 +39,50 @@ interface IGroupsProps {
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
seeMore: {
|
||||
marginTop: theme.spacing(3)
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
paper: {
|
||||
// padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column"
|
||||
flexDirection: "column",
|
||||
},
|
||||
addSideBar: {
|
||||
width: "320px",
|
||||
padding: "20px"
|
||||
padding: "20px",
|
||||
},
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
tableToolbar: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(0)
|
||||
paddingRight: theme.spacing(0),
|
||||
},
|
||||
wrapCell: {
|
||||
maxWidth: "200px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word"
|
||||
wordWrap: "break-word",
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
"& tr": {
|
||||
"& th": {
|
||||
fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
actionsTray: {
|
||||
textAlign: "right",
|
||||
"& button": {
|
||||
marginLeft: 10
|
||||
}
|
||||
marginLeft: 10,
|
||||
},
|
||||
},
|
||||
searchField: {
|
||||
background: "#FFFFFF",
|
||||
padding: 12,
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 3px 6px #00000012"
|
||||
}
|
||||
boxShadow: "0px 3px 6px #00000012",
|
||||
},
|
||||
});
|
||||
|
||||
const Groups = ({ classes }: IGroupsProps) => {
|
||||
@@ -126,7 +126,7 @@ const Groups = ({ classes }: IGroupsProps) => {
|
||||
.then((res: GroupsList) => {
|
||||
let resGroups: string[] = [];
|
||||
if (res.groups !== null) {
|
||||
resGroups = res.groups.sort(groupsSort);
|
||||
resGroups = res.groups.sort(stringSort);
|
||||
}
|
||||
setRecords(resGroups);
|
||||
const total = !res.total ? 0 : res.total;
|
||||
@@ -140,7 +140,7 @@ const Groups = ({ classes }: IGroupsProps) => {
|
||||
setPage(newPage);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setError(err);
|
||||
isLoading(false);
|
||||
});
|
||||
@@ -162,7 +162,7 @@ const Groups = ({ classes }: IGroupsProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const filteredRecords = records.filter(elementItem =>
|
||||
const filteredRecords = records.filter((elementItem) =>
|
||||
elementItem.includes(filter)
|
||||
);
|
||||
|
||||
@@ -178,7 +178,7 @@ const Groups = ({ classes }: IGroupsProps) => {
|
||||
|
||||
const tableActions = [
|
||||
{ type: "view", onClick: viewAction },
|
||||
{ type: "delete", onClick: deleteAction }
|
||||
{ type: "delete", onClick: deleteAction },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -217,9 +217,9 @@ const Groups = ({ classes }: IGroupsProps) => {
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
)
|
||||
),
|
||||
}}
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
setFilter(e.target.value);
|
||||
}}
|
||||
/>
|
||||
@@ -255,11 +255,11 @@ const Groups = ({ classes }: IGroupsProps) => {
|
||||
page: page,
|
||||
SelectProps: {
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true
|
||||
native: true,
|
||||
},
|
||||
onChangePage: handleChangePage,
|
||||
onChangeRowsPerPage: handleChangeRowsPerPage,
|
||||
ActionsComponent: MinTablePaginationActions
|
||||
ActionsComponent: MinTablePaginationActions,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -38,43 +38,43 @@ interface IGroupsProps {
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
seeMore: {
|
||||
marginTop: theme.spacing(3)
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
paper: {
|
||||
// padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column"
|
||||
flexDirection: "column",
|
||||
},
|
||||
addSideBar: {
|
||||
width: "320px",
|
||||
padding: "20px"
|
||||
padding: "20px",
|
||||
},
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
tableToolbar: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(0)
|
||||
paddingRight: theme.spacing(0),
|
||||
},
|
||||
wrapCell: {
|
||||
maxWidth: "200px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word"
|
||||
wordWrap: "break-word",
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
"& tr": {
|
||||
"& th": {
|
||||
fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
actionsTray: {
|
||||
textAlign: "left",
|
||||
"& button": {
|
||||
marginLeft: 10
|
||||
}
|
||||
marginLeft: 10,
|
||||
},
|
||||
},
|
||||
filterField: {
|
||||
background: "#FFFFFF",
|
||||
@@ -82,24 +82,24 @@ const styles = (theme: Theme) =>
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 3px 6px #00000012",
|
||||
width: "100%",
|
||||
zIndex: 500
|
||||
zIndex: 500,
|
||||
},
|
||||
noFound: {
|
||||
textAlign: "center",
|
||||
padding: "10px 0"
|
||||
padding: "10px 0",
|
||||
},
|
||||
tableContainer: {
|
||||
maxHeight: 250
|
||||
maxHeight: 250,
|
||||
},
|
||||
stickyHeader: {
|
||||
backgroundColor: "#fff"
|
||||
}
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
});
|
||||
|
||||
const UsersSelectors = ({
|
||||
classes,
|
||||
selectedUsers,
|
||||
setSelectedUsers
|
||||
setSelectedUsers,
|
||||
}: IGroupsProps) => {
|
||||
//Local States
|
||||
const [records, setRecords] = useState<any[]>([]);
|
||||
@@ -133,7 +133,7 @@ const UsersSelectors = ({
|
||||
elements.push(value);
|
||||
} else {
|
||||
// User has unchecked this field, we need to remove it from the list
|
||||
elements = elements.filter(element => element !== value);
|
||||
elements = elements.filter((element) => element !== value);
|
||||
}
|
||||
setSelectedUsers(elements);
|
||||
|
||||
@@ -154,13 +154,13 @@ const UsersSelectors = ({
|
||||
setError("");
|
||||
isLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setError(err);
|
||||
isLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const filteredRecords = records.filter(elementItem =>
|
||||
const filteredRecords = records.filter((elementItem) =>
|
||||
elementItem.accessKey.includes(filter)
|
||||
);
|
||||
|
||||
@@ -185,9 +185,9 @@ const UsersSelectors = ({
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
)
|
||||
),
|
||||
}}
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
setFilter(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
268
portal-ui/src/screens/Console/Logs/Logs.tsx
Normal file
268
portal-ui/src/screens/Console/Logs/Logs.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React, { useEffect } from "react";
|
||||
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
|
||||
import { AppState } from "../../../store";
|
||||
import { connect } from "react-redux";
|
||||
import { logMessageReceived, logResetMessages } from "./actions";
|
||||
import { LogMessage } from "./types";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { timeFromDate } from "../../../common/utils";
|
||||
import { isNullOrUndefined } from "util";
|
||||
import { wsProtocol } from "../../../utils/wsUtils";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
logList: {
|
||||
background: "white",
|
||||
maxHeight: "400px",
|
||||
overflow: "auto",
|
||||
"& ul": {
|
||||
margin: "4px",
|
||||
padding: "0px",
|
||||
},
|
||||
"& ul li": {
|
||||
listStyle: "none",
|
||||
margin: "0px",
|
||||
padding: "0px",
|
||||
borderBottom: "1px solid #dedede",
|
||||
},
|
||||
},
|
||||
tab: {
|
||||
padding: "25px",
|
||||
},
|
||||
logerror: {
|
||||
color: "#A52A2A",
|
||||
},
|
||||
logerror_tab: {
|
||||
color: "#A52A2A",
|
||||
padding: "25px",
|
||||
},
|
||||
ansidefault: {
|
||||
color: "black",
|
||||
},
|
||||
});
|
||||
|
||||
interface ILogs {
|
||||
classes: any;
|
||||
logMessageReceived: typeof logMessageReceived;
|
||||
logResetMessages: typeof logResetMessages;
|
||||
messages: LogMessage[];
|
||||
}
|
||||
|
||||
const Logs = ({
|
||||
classes,
|
||||
logMessageReceived,
|
||||
logResetMessages,
|
||||
messages,
|
||||
}: ILogs) => {
|
||||
useEffect(() => {
|
||||
logResetMessages();
|
||||
const url = new URL(window.location.toString());
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
const port = isDev ? "9090" : url.port;
|
||||
|
||||
const wsProt = wsProtocol(url.protocol);
|
||||
|
||||
const c = new W3CWebSocket(
|
||||
`${wsProt}://${url.hostname}:${port}/ws/console`
|
||||
);
|
||||
|
||||
let interval: any | null = null;
|
||||
if (c !== null) {
|
||||
c.onopen = () => {
|
||||
console.log("WebSocket Client Connected");
|
||||
c.send("ok");
|
||||
interval = setInterval(() => {
|
||||
c.send("ok");
|
||||
}, 10 * 1000);
|
||||
};
|
||||
c.onmessage = (message: IMessageEvent) => {
|
||||
// console.log(message.data.toString())
|
||||
let m: LogMessage = JSON.parse(message.data.toString());
|
||||
m.time = new Date(m.time.toString());
|
||||
m.key = Math.random();
|
||||
logMessageReceived(m);
|
||||
};
|
||||
c.onclose = () => {
|
||||
clearInterval(interval);
|
||||
console.log("connection closed by server");
|
||||
};
|
||||
return () => {
|
||||
c.close(1000);
|
||||
clearInterval(interval);
|
||||
console.log("closing websockets");
|
||||
};
|
||||
}
|
||||
}, [logMessageReceived]);
|
||||
|
||||
// replaces a character of a string with other at a given index
|
||||
const replaceWeirdChar = (
|
||||
origString: string,
|
||||
replaceChar: string,
|
||||
index: number
|
||||
) => {
|
||||
let firstPart = origString.substr(0, index);
|
||||
let lastPart = origString.substr(index + 1);
|
||||
|
||||
let newString = firstPart + replaceChar + lastPart;
|
||||
return newString;
|
||||
};
|
||||
|
||||
const renderError = (logElement: LogMessage) => {
|
||||
let errorElems = [];
|
||||
if (!isNullOrUndefined(logElement.error)) {
|
||||
if (logElement.api && logElement.api.name) {
|
||||
errorElems.push(
|
||||
<li key={`api-${logElement.key}`}>
|
||||
<span className={classes.logerror}>API: {logElement.api.name}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
if (logElement.time) {
|
||||
errorElems.push(
|
||||
<li key={`time-${logElement.key}`}>
|
||||
<span className={classes.logerror}>
|
||||
Time: {timeFromDate(logElement.time)}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
if (logElement.deploymentid) {
|
||||
errorElems.push(
|
||||
<li key={`deploytmentid-${logElement.key}`}>
|
||||
<span className={classes.logerror}>
|
||||
DeploymentID: {logElement.deploymentid}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
if (logElement.requestID) {
|
||||
errorElems.push(
|
||||
<li key={`requestid-${logElement.key}`}>
|
||||
<span className={classes.logerror}>
|
||||
RequestID: {logElement.requestID}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
if (logElement.remotehost) {
|
||||
errorElems.push(
|
||||
<li key={`remotehost-${logElement.key}`}>
|
||||
<span className={classes.logerror}>
|
||||
RemoteHost: {logElement.remotehost}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
if (logElement.host) {
|
||||
errorElems.push(
|
||||
<li key={`host-${logElement.key}`}>
|
||||
<span className={classes.logerror}>Host: {logElement.host}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
if (logElement.userAgent) {
|
||||
errorElems.push(
|
||||
<li key={`useragent-${logElement.key}`}>
|
||||
<span className={classes.logerror}>
|
||||
UserAgent: {logElement.userAgent}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
if (logElement.error.message) {
|
||||
errorElems.push(
|
||||
<li key={`message-${logElement.key}`}>
|
||||
<span className={classes.logerror}>
|
||||
Error: {logElement.error.message}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
if (logElement.error.source) {
|
||||
// for all sources add padding
|
||||
for (let s in logElement.error.source) {
|
||||
errorElems.push(
|
||||
<li key={`source-${logElement.key}-${s}`}>
|
||||
<span className={classes.logerror_tab}>
|
||||
{logElement.error.source[s]}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errorElems;
|
||||
};
|
||||
|
||||
const renderLog = (logElement: LogMessage) => {
|
||||
let logMessage = logElement.ConsoleMsg;
|
||||
// remove any non ascii characters, exclude any control codes
|
||||
logMessage = logMessage.replace(/([^\x20-\x7F])/g, "");
|
||||
|
||||
// regex for terminal colors like e.g. `[31;4m `
|
||||
const tColorRegex = /((\[[0-9;]+m))/g;
|
||||
|
||||
// get substring if there was a match for to split what
|
||||
// is going to be colored and what not, here we add color
|
||||
// only to the first match.
|
||||
let substr = logMessage.replace(tColorRegex, "");
|
||||
|
||||
// if starts with multiple spaces add padding
|
||||
if (substr.startsWith(" ")) {
|
||||
return (
|
||||
<li key={logElement.key}>
|
||||
<span className={classes.tab}>{substr}</span>
|
||||
</li>
|
||||
);
|
||||
} else if (!isNullOrUndefined(logElement.error)) {
|
||||
// list error message and all sources and error elems
|
||||
return renderError(logElement);
|
||||
} else {
|
||||
// for all remaining set default class
|
||||
return (
|
||||
<li key={logElement.key}>
|
||||
<span className={classes.ansidefault}>{substr}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Logs</h1>
|
||||
<div className={classes.logList}>
|
||||
<ul>
|
||||
{messages.map((m) => {
|
||||
return renderLog(m);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapState = (state: AppState) => ({
|
||||
messages: state.logs.messages,
|
||||
});
|
||||
|
||||
const connector = connect(mapState, {
|
||||
logMessageReceived: logMessageReceived,
|
||||
logResetMessages: logResetMessages,
|
||||
});
|
||||
|
||||
export default connector(withStyles(styles)(Logs));
|
||||
44
portal-ui/src/screens/Console/Logs/actions.ts
Normal file
44
portal-ui/src/screens/Console/Logs/actions.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { LogMessage } from "./types";
|
||||
|
||||
export const LOG_MESSAGE_RECEIVED = "LOG_MESSAGE_RECEIVED";
|
||||
export const LOG_RESET_MESSAGES = "LOG_RESET_MESSAGES";
|
||||
|
||||
interface LogMessageReceivedAction {
|
||||
type: typeof LOG_MESSAGE_RECEIVED;
|
||||
message: LogMessage;
|
||||
}
|
||||
|
||||
interface LogResetMessagesAction {
|
||||
type: typeof LOG_RESET_MESSAGES;
|
||||
}
|
||||
|
||||
export type LogActionTypes = LogMessageReceivedAction | LogResetMessagesAction;
|
||||
|
||||
export function logMessageReceived(message: LogMessage) {
|
||||
return {
|
||||
type: LOG_MESSAGE_RECEIVED,
|
||||
message: message,
|
||||
};
|
||||
}
|
||||
|
||||
export function logResetMessages() {
|
||||
return {
|
||||
type: LOG_RESET_MESSAGES,
|
||||
};
|
||||
}
|
||||
50
portal-ui/src/screens/Console/Logs/reducers.ts
Normal file
50
portal-ui/src/screens/Console/Logs/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 {
|
||||
LOG_MESSAGE_RECEIVED,
|
||||
LOG_RESET_MESSAGES,
|
||||
LogActionTypes,
|
||||
} from "./actions";
|
||||
import { LogMessage } from "./types";
|
||||
|
||||
export interface LogState {
|
||||
messages: LogMessage[];
|
||||
}
|
||||
|
||||
const initialState: LogState = {
|
||||
messages: [],
|
||||
};
|
||||
|
||||
export function logReducer(
|
||||
state = initialState,
|
||||
action: LogActionTypes
|
||||
): LogState {
|
||||
switch (action.type) {
|
||||
case LOG_MESSAGE_RECEIVED:
|
||||
return {
|
||||
...state,
|
||||
messages: [...state.messages, action.message],
|
||||
};
|
||||
case LOG_RESET_MESSAGES:
|
||||
return {
|
||||
...state,
|
||||
messages: [],
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
44
portal-ui/src/screens/Console/Logs/types.ts
Normal file
44
portal-ui/src/screens/Console/Logs/types.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
export interface logError {
|
||||
message: string;
|
||||
source: string[];
|
||||
}
|
||||
|
||||
export interface logErrorApiArgs {
|
||||
bucket: string;
|
||||
object: string;
|
||||
}
|
||||
|
||||
export interface logErrorApi {
|
||||
name: string;
|
||||
args: logErrorApiArgs;
|
||||
}
|
||||
|
||||
export interface LogMessage {
|
||||
remotehost: string;
|
||||
host: string;
|
||||
requestID: string;
|
||||
userAgent: string;
|
||||
message: string;
|
||||
api: logErrorApi;
|
||||
deploymentid: string;
|
||||
time: Date;
|
||||
error: logError;
|
||||
ConsoleMsg: string;
|
||||
key: number;
|
||||
}
|
||||
@@ -17,6 +17,9 @@
|
||||
import React from "react";
|
||||
import ListItem from "@material-ui/core/ListItem";
|
||||
import ListItemIcon from "@material-ui/core/ListItemIcon";
|
||||
import RoomServiceIcon from "@material-ui/icons/RoomService";
|
||||
import WebAssetIcon from "@material-ui/icons/WebAsset";
|
||||
import CenterFocusWeakIcon from "@material-ui/icons/CenterFocusWeak";
|
||||
import ListItemText from "@material-ui/core/ListItemText";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { Divider, Typography, withStyles } from "@material-ui/core";
|
||||
@@ -32,7 +35,7 @@ import {
|
||||
BucketsIcon,
|
||||
DashboardIcon,
|
||||
PermissionIcon,
|
||||
UsersIcon
|
||||
UsersIcon,
|
||||
} from "../../icons";
|
||||
import { createStyles, Theme } from "@material-ui/core/styles";
|
||||
import PersonIcon from "@material-ui/icons/Person";
|
||||
@@ -48,8 +51,8 @@ const styles = (theme: Theme) =>
|
||||
marginBottom: "20px",
|
||||
textAlign: "center",
|
||||
"& img": {
|
||||
width: "120px"
|
||||
}
|
||||
width: "120px",
|
||||
},
|
||||
},
|
||||
menuList: {
|
||||
"& .active": {
|
||||
@@ -59,31 +62,31 @@ const styles = (theme: Theme) =>
|
||||
background:
|
||||
"transparent linear-gradient(90deg, #362585 0%, #362585 7%, #281B6F 39%, #1F1661 100%) 0% 0% no-repeat padding-box",
|
||||
"& .MuiSvgIcon-root": {
|
||||
color: "white"
|
||||
}
|
||||
color: "white",
|
||||
},
|
||||
},
|
||||
"& .MuiListItem-root": {
|
||||
marginTop: "16px"
|
||||
marginTop: "16px",
|
||||
},
|
||||
paddingLeft: "30px",
|
||||
"& .MuiSvgIcon-root": {
|
||||
fontSize: "18px",
|
||||
color: "#393939"
|
||||
color: "#393939",
|
||||
},
|
||||
"& .MuiListItemIcon-root": {
|
||||
minWidth: "40px"
|
||||
minWidth: "40px",
|
||||
},
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "14px"
|
||||
fontSize: "14px",
|
||||
},
|
||||
"& .MuiListItem-gutters": {
|
||||
paddingRight: "0px"
|
||||
}
|
||||
}
|
||||
paddingRight: "0px",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mapState = (state: AppState) => ({
|
||||
open: state.system.loggedIn
|
||||
open: state.system.loggedIn,
|
||||
});
|
||||
|
||||
const connector = connect(mapState, { userLoggedIn });
|
||||
@@ -91,6 +94,7 @@ const connector = connect(mapState, { userLoggedIn });
|
||||
interface MenuProps {
|
||||
classes: any;
|
||||
userLoggedIn: typeof userLoggedIn;
|
||||
pages: string[];
|
||||
}
|
||||
|
||||
class Menu extends React.Component<MenuProps> {
|
||||
@@ -112,65 +116,161 @@ class Menu extends React.Component<MenuProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const { classes, pages } = this.props;
|
||||
const allowedPages = pages.reduce((result: any, item: any, index: any) => {
|
||||
result[item] = true;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const menu = [
|
||||
{
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: "/dashboard",
|
||||
name: "Dashboard",
|
||||
icon: <DashboardIcon />,
|
||||
},
|
||||
{
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: "/buckets",
|
||||
name: "Buckets",
|
||||
icon: <BucketsIcon />,
|
||||
},
|
||||
{
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: "/service-accounts",
|
||||
name: "Service Accounts",
|
||||
icon: <RoomServiceIcon />,
|
||||
forceDisplay: true,
|
||||
},
|
||||
{
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: "/watch",
|
||||
name: "Watch",
|
||||
icon: <CenterFocusWeakIcon />,
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
key: "divider-1",
|
||||
},
|
||||
{
|
||||
type: "title",
|
||||
name: "Admin",
|
||||
component: Typography,
|
||||
},
|
||||
{
|
||||
group: "Admin",
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: "/users",
|
||||
name: "Users",
|
||||
icon: <PersonIcon />,
|
||||
},
|
||||
{
|
||||
group: "Admin",
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: "/groups",
|
||||
name: "Groups",
|
||||
icon: <UsersIcon />,
|
||||
},
|
||||
{
|
||||
group: "Admin",
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: "/policies",
|
||||
name: "IAM Policies",
|
||||
icon: <PermissionIcon />,
|
||||
},
|
||||
{
|
||||
group: "Admin",
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: "/trace",
|
||||
name: "Trace",
|
||||
icon: <LoopIcon />,
|
||||
},
|
||||
{
|
||||
group: "Admin",
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: "/logs",
|
||||
name: "Console Logs",
|
||||
icon: <WebAssetIcon />,
|
||||
},
|
||||
{
|
||||
type: "title",
|
||||
name: "Configuration",
|
||||
component: Typography,
|
||||
},
|
||||
{
|
||||
group: "Configuration",
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: "/notification-endpoints",
|
||||
name: "Lambda Notifications",
|
||||
icon: <NotificationsIcon />,
|
||||
},
|
||||
{
|
||||
group: "Configuration",
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: "/configurations-list",
|
||||
name: "Configurations List",
|
||||
icon: <ListAltIcon />,
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
key: "divider-2",
|
||||
},
|
||||
];
|
||||
|
||||
const allowedItems = menu.filter(
|
||||
(item: any) =>
|
||||
allowedPages[item.to] || item.forceDisplay || item.type !== "item"
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={classes.logo}>
|
||||
<img src={logo} alt="logo" />
|
||||
</div>
|
||||
<List className={classes.menuList}>
|
||||
<ListItem button component={NavLink} to="/dashboard">
|
||||
<ListItemIcon>
|
||||
<DashboardIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Dashboard" />
|
||||
</ListItem>
|
||||
<ListItem button component={NavLink} to="/buckets">
|
||||
<ListItemIcon>
|
||||
<BucketsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Buckets" />
|
||||
</ListItem>
|
||||
<Divider />
|
||||
<ListItem component={Typography}>Admin</ListItem>
|
||||
<ListItem button component={NavLink} to="/users">
|
||||
<ListItemIcon>
|
||||
<PersonIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Users" />
|
||||
</ListItem>
|
||||
<ListItem button component={NavLink} to="/groups">
|
||||
<ListItemIcon>
|
||||
<UsersIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Groups" />
|
||||
</ListItem>
|
||||
<ListItem button component={NavLink} to="/policies">
|
||||
<ListItemIcon>
|
||||
<PermissionIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="IAM Policies" />
|
||||
</ListItem>
|
||||
<ListItem button component={NavLink} to="/trace">
|
||||
<ListItemIcon>
|
||||
<LoopIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Trace" />
|
||||
</ListItem>
|
||||
<ListItem component={Typography}>Configuration</ListItem>
|
||||
<ListItem button component={NavLink} to="/notification-endpoints">
|
||||
<ListItemIcon>
|
||||
<NotificationsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Lambda Notifications" />
|
||||
</ListItem>
|
||||
<ListItem button component={NavLink} to="/configurations-list">
|
||||
<ListItemIcon>
|
||||
<ListAltIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Configurations List" />
|
||||
</ListItem>
|
||||
<Divider />
|
||||
{allowedItems.map((page: any) => {
|
||||
switch (page.type) {
|
||||
case "divider": {
|
||||
return <Divider key={page.key} />;
|
||||
}
|
||||
case "item": {
|
||||
return (
|
||||
<ListItem
|
||||
key={page.to}
|
||||
button
|
||||
component={page.component}
|
||||
to={page.to}
|
||||
>
|
||||
{page.icon && <ListItemIcon>{page.icon}</ListItemIcon>}
|
||||
{page.name && <ListItemText primary={page.name} />}
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
case "title": {
|
||||
return (
|
||||
(allowedItems || []).filter(
|
||||
(item: any) => item.group === page.name
|
||||
).length > 0 && (
|
||||
<ListItem key={page.name} component={page.component}>
|
||||
{page.name}
|
||||
</ListItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
default:
|
||||
}
|
||||
})}
|
||||
<ListItem
|
||||
button
|
||||
onClick={() => {
|
||||
|
||||
@@ -39,27 +39,27 @@ import {
|
||||
notifyElasticsearch,
|
||||
notifyWebhooks,
|
||||
notifyNsq,
|
||||
removeEmptyFields
|
||||
removeEmptyFields,
|
||||
} from "../Configurations/utils";
|
||||
import { IElementValue } from "../Configurations/types";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
strongText: {
|
||||
fontWeight: 700
|
||||
fontWeight: 700,
|
||||
},
|
||||
keyName: {
|
||||
marginLeft: 5
|
||||
marginLeft: 5,
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right"
|
||||
textAlign: "right",
|
||||
},
|
||||
logoButton: {
|
||||
height: "80px"
|
||||
}
|
||||
height: "80px",
|
||||
},
|
||||
});
|
||||
|
||||
interface IAddNotificationEndpointProps {
|
||||
@@ -73,7 +73,7 @@ const AddNotificationEndpoint = ({
|
||||
open,
|
||||
closeModalAndRefresh,
|
||||
serverNeedsRestart,
|
||||
classes
|
||||
classes,
|
||||
}: IAddNotificationEndpointProps) => {
|
||||
//Local States
|
||||
const [service, setService] = useState<string>("");
|
||||
@@ -86,18 +86,18 @@ const AddNotificationEndpoint = ({
|
||||
useEffect(() => {
|
||||
if (saving) {
|
||||
const payload = {
|
||||
key_values: removeEmptyFields(valuesArr)
|
||||
key_values: removeEmptyFields(valuesArr),
|
||||
};
|
||||
api
|
||||
.invoke("PUT", `/api/v1/configs/${service}`, payload)
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
setSaving(false);
|
||||
setError("");
|
||||
serverNeedsRestart(true);
|
||||
|
||||
closeModalAndRefresh();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setSaving(false);
|
||||
setError(err);
|
||||
});
|
||||
@@ -111,7 +111,7 @@ const AddNotificationEndpoint = ({
|
||||
};
|
||||
|
||||
const onValueChange = useCallback(
|
||||
newValue => {
|
||||
(newValue) => {
|
||||
setValueArr(newValue);
|
||||
},
|
||||
[setValueArr]
|
||||
@@ -342,14 +342,14 @@ const AddNotificationEndpoint = ({
|
||||
</Grid>
|
||||
)}
|
||||
<form noValidate onSubmit={submitForm}>
|
||||
{srvComponent}
|
||||
|
||||
<Grid item xs={3} className={classes.buttonContainer}>
|
||||
<Grid item xs={12}>
|
||||
{srvComponent}
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
disabled={saving}
|
||||
>
|
||||
Save
|
||||
|
||||
@@ -31,14 +31,14 @@ import {
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Select,
|
||||
TextField
|
||||
TextField,
|
||||
} from "@material-ui/core";
|
||||
import {
|
||||
createStyles,
|
||||
lighten,
|
||||
makeStyles,
|
||||
Theme,
|
||||
withStyles
|
||||
withStyles,
|
||||
} from "@material-ui/core/styles";
|
||||
import api from "../../../common/api";
|
||||
import clsx from "clsx";
|
||||
@@ -61,21 +61,21 @@ const useToolbarStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(1)
|
||||
paddingRight: theme.spacing(1),
|
||||
},
|
||||
highlight:
|
||||
theme.palette.type === "light"
|
||||
? {
|
||||
color: theme.palette.secondary.main,
|
||||
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
|
||||
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
|
||||
}
|
||||
: {
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor: theme.palette.secondary.dark
|
||||
backgroundColor: theme.palette.secondary.dark,
|
||||
},
|
||||
title: {
|
||||
flex: "1 1 100%"
|
||||
}
|
||||
flex: "1 1 100%",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -90,7 +90,7 @@ const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
|
||||
return (
|
||||
<Toolbar
|
||||
className={clsx(classes.root, {
|
||||
[classes.highlight]: numSelected > 0
|
||||
[classes.highlight]: numSelected > 0,
|
||||
})}
|
||||
>
|
||||
{numSelected > 0 ? (
|
||||
@@ -122,8 +122,8 @@ const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
}
|
||||
color: "red",
|
||||
},
|
||||
});
|
||||
|
||||
interface IAddPermissionContentProps {
|
||||
@@ -164,7 +164,7 @@ class AddPermissionContent extends React.Component<
|
||||
resources: [],
|
||||
buckets: [],
|
||||
bucketsError: "",
|
||||
loadingBuckets: false
|
||||
loadingBuckets: false,
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -176,10 +176,10 @@ class AddPermissionContent extends React.Component<
|
||||
this.setState({
|
||||
loadingBuckets: false,
|
||||
buckets: res.buckets,
|
||||
bucketsError: ""
|
||||
bucketsError: "",
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.setState({ loadingBuckets: false, bucketsError: err });
|
||||
});
|
||||
});
|
||||
@@ -190,8 +190,8 @@ class AddPermissionContent extends React.Component<
|
||||
name: selectedPermission.name,
|
||||
description: selectedPermission.description,
|
||||
effect: selectedPermission.effect,
|
||||
resources: selectedPermission.resources.map(r => r.bucket_name),
|
||||
action: selectedPermission.actions[0].type
|
||||
resources: selectedPermission.resources.map((r) => r.bucket_name),
|
||||
action: selectedPermission.actions[0].type,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ class AddPermissionContent extends React.Component<
|
||||
resources,
|
||||
description,
|
||||
effect,
|
||||
action
|
||||
action,
|
||||
} = this.state;
|
||||
const { selectedPermission } = this.props;
|
||||
if (addLoading) {
|
||||
@@ -219,23 +219,23 @@ class AddPermissionContent extends React.Component<
|
||||
description: description,
|
||||
effect: effect,
|
||||
resources: resources,
|
||||
actions: [action]
|
||||
actions: [action],
|
||||
})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
this.setState(
|
||||
{
|
||||
addLoading: false,
|
||||
addError: ""
|
||||
addError: "",
|
||||
},
|
||||
() => {
|
||||
this.props.closeModalAndRefresh();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
addError: err,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@@ -245,23 +245,23 @@ class AddPermissionContent extends React.Component<
|
||||
description: description,
|
||||
effect: effect,
|
||||
resources: resources,
|
||||
actions: [action]
|
||||
actions: [action],
|
||||
})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
this.setState(
|
||||
{
|
||||
addLoading: false,
|
||||
addError: ""
|
||||
addError: "",
|
||||
},
|
||||
() => {
|
||||
this.props.closeModalAndRefresh();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
addError: err,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -280,14 +280,14 @@ class AddPermissionContent extends React.Component<
|
||||
name,
|
||||
description,
|
||||
effect,
|
||||
action
|
||||
action,
|
||||
} = this.state;
|
||||
|
||||
const handleSelectAllClick = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (event.target.checked) {
|
||||
const newSelecteds = buckets.map(n => n.name);
|
||||
const newSelecteds = buckets.map((n) => n.name);
|
||||
this.setState({ resources: newSelecteds });
|
||||
return;
|
||||
}
|
||||
@@ -432,7 +432,7 @@ class AddPermissionContent extends React.Component<
|
||||
}
|
||||
onChange={handleSelectAllClick}
|
||||
inputProps={{
|
||||
"aria-label": "select all desserts"
|
||||
"aria-label": "select all desserts",
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
@@ -452,7 +452,9 @@ class AddPermissionContent extends React.Component<
|
||||
return (
|
||||
<TableRow
|
||||
hover
|
||||
onClick={event => handleClick(event, row.name)}
|
||||
onClick={(event) =>
|
||||
handleClick(event, row.name)
|
||||
}
|
||||
role="checkbox"
|
||||
aria-checked={isItemSelected}
|
||||
tabIndex={-1}
|
||||
@@ -500,7 +502,7 @@ class AddPermissionContent extends React.Component<
|
||||
value={action}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
action: (event.target as HTMLInputElement).value
|
||||
action: (event.target as HTMLInputElement).value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -24,6 +24,7 @@ import api from "../../../common/api";
|
||||
import "codemirror/lib/codemirror.css";
|
||||
import "codemirror/theme/material.css";
|
||||
import { Policy } from "./types";
|
||||
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
|
||||
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
|
||||
@@ -32,18 +33,19 @@ require("codemirror/mode/javascript/javascript");
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
jsonPolicyEditor: {
|
||||
minHeight: 400,
|
||||
width: "100%"
|
||||
width: "100%",
|
||||
},
|
||||
codeMirror: {
|
||||
fontSize: 14
|
||||
fontSize: 14,
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right"
|
||||
}
|
||||
textAlign: "right",
|
||||
},
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
interface IAddPolicyProps {
|
||||
@@ -65,7 +67,7 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
|
||||
addLoading: false,
|
||||
addError: "",
|
||||
policyName: "",
|
||||
policyDefinition: ""
|
||||
policyDefinition: "",
|
||||
};
|
||||
|
||||
addRecord(event: React.FormEvent) {
|
||||
@@ -78,23 +80,23 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
|
||||
api
|
||||
.invoke("POST", "/api/v1/policies", {
|
||||
name: policyName,
|
||||
policy: policyDefinition
|
||||
policy: policyDefinition,
|
||||
})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
this.setState(
|
||||
{
|
||||
addLoading: false,
|
||||
addError: ""
|
||||
addError: "",
|
||||
},
|
||||
() => {
|
||||
this.props.closeModalAndRefresh();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
addError: err,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -105,7 +107,7 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
|
||||
|
||||
if (policyEdit) {
|
||||
this.setState({
|
||||
policyName: policyEdit.name
|
||||
policyName: policyEdit.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -131,51 +133,50 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
<InputBoxWrapper
|
||||
id="policy-name"
|
||||
name="policy-name"
|
||||
label="Policy Name"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ policyName: e.target.value });
|
||||
}}
|
||||
value={policyName}
|
||||
disabled={!!policyEdit}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<CodeMirror
|
||||
className={classes.codeMirror}
|
||||
value={
|
||||
policyEdit
|
||||
? JSON.stringify(JSON.parse(policyEdit.policy), null, 4)
|
||||
: ""
|
||||
}
|
||||
options={{
|
||||
mode: "javascript",
|
||||
lineNumbers: true,
|
||||
}}
|
||||
onChange={(editor, data, value) => {
|
||||
this.setState({ policyDefinition: value });
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="policy-name"
|
||||
name="policy-name"
|
||||
label="Policy Name"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ policyName: e.target.value });
|
||||
}}
|
||||
value={policyName}
|
||||
disabled={!!policyEdit}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<CodeMirror
|
||||
className={classes.codeMirror}
|
||||
value={
|
||||
policyEdit
|
||||
? JSON.stringify(JSON.parse(policyEdit.policy), null, 4)
|
||||
: ""
|
||||
}
|
||||
options={{
|
||||
mode: "javascript",
|
||||
lineNumbers: true
|
||||
}}
|
||||
onChange={(editor, data, value) => {
|
||||
this.setState({ policyDefinition: value });
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
{!policyEdit && (
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2019 MinIO, Inc.
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -14,363 +14,101 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import { UnControlled as CodeMirror } from "react-codemirror2";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { Button, FormControlLabel, LinearProgress } from "@material-ui/core";
|
||||
import {
|
||||
createStyles,
|
||||
lighten,
|
||||
makeStyles,
|
||||
Theme,
|
||||
withStyles
|
||||
} from "@material-ui/core/styles";
|
||||
import api from "../../../common/api";
|
||||
import clsx from "clsx";
|
||||
import Table from "@material-ui/core/Table";
|
||||
import TableBody from "@material-ui/core/TableBody";
|
||||
import TableCell from "@material-ui/core/TableCell";
|
||||
import TableContainer from "@material-ui/core/TableContainer";
|
||||
import TableHead from "@material-ui/core/TableHead";
|
||||
import TablePagination from "@material-ui/core/TablePagination";
|
||||
import TableRow from "@material-ui/core/TableRow";
|
||||
import Toolbar from "@material-ui/core/Toolbar";
|
||||
import Checkbox from "@material-ui/core/Checkbox";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import Tooltip from "@material-ui/core/Tooltip";
|
||||
import FilterListIcon from "@material-ui/icons/FilterList";
|
||||
import { Permission, PermissionList } from "../Permissions/types";
|
||||
import {
|
||||
NewServiceAccount,
|
||||
ServiceAccount,
|
||||
ServiceAccountDetails
|
||||
} from "./types";
|
||||
import Switch from "@material-ui/core/Switch";
|
||||
import { Button, LinearProgress, Tooltip } from "@material-ui/core";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
|
||||
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
import api from "../../../common/api";
|
||||
import "codemirror/lib/codemirror.css";
|
||||
import "codemirror/theme/material.css";
|
||||
import { NewServiceAccount } from "./types";
|
||||
import HelpIcon from "@material-ui/icons/Help";
|
||||
|
||||
const useToolbarStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(1)
|
||||
},
|
||||
highlight:
|
||||
theme.palette.type === "light"
|
||||
? {
|
||||
color: theme.palette.secondary.main,
|
||||
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
|
||||
}
|
||||
: {
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor: theme.palette.secondary.dark
|
||||
},
|
||||
title: {
|
||||
flex: "1 1 100%"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
interface EnhancedTableToolbarProps {
|
||||
numSelected: number;
|
||||
}
|
||||
|
||||
const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
|
||||
const classes = useToolbarStyles();
|
||||
const { numSelected } = props;
|
||||
|
||||
return (
|
||||
<Toolbar
|
||||
className={clsx(classes.root, {
|
||||
[classes.highlight]: numSelected > 0
|
||||
})}
|
||||
>
|
||||
{numSelected > 0 ? (
|
||||
<Typography
|
||||
className={classes.title}
|
||||
color="inherit"
|
||||
variant="subtitle1"
|
||||
>
|
||||
{numSelected} selected
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography className={classes.title} variant="h6" id="tableTitle">
|
||||
Permissions
|
||||
</Typography>
|
||||
)}
|
||||
{numSelected > 0 ? (
|
||||
<span />
|
||||
) : (
|
||||
<Tooltip title="Filter list">
|
||||
<IconButton aria-label="filter list">
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Toolbar>
|
||||
);
|
||||
};
|
||||
require("codemirror/mode/javascript/javascript");
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
}
|
||||
color: "red",
|
||||
},
|
||||
jsonPolicyEditor: {
|
||||
minHeight: 400,
|
||||
width: "100%",
|
||||
},
|
||||
codeMirror: {
|
||||
fontSize: 14,
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right",
|
||||
},
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
interface IAddServiceAccountContentProps {
|
||||
interface IAddServiceAccountProps {
|
||||
classes: any;
|
||||
open: boolean;
|
||||
closeModalAndRefresh: (res: NewServiceAccount | null) => void;
|
||||
selectedServiceAccount: ServiceAccount | null;
|
||||
}
|
||||
|
||||
interface IAddServiceAccountContentState {
|
||||
addLoading: boolean;
|
||||
addError: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
selectedPermissions: Permission[];
|
||||
rowsPerPage: number;
|
||||
page: number;
|
||||
permissions: Permission[];
|
||||
permissionsError: string;
|
||||
loadingPermissions: boolean;
|
||||
loadingServiceAccount: boolean;
|
||||
}
|
||||
const AddServiceAccount = ({
|
||||
classes,
|
||||
open,
|
||||
closeModalAndRefresh,
|
||||
}: IAddServiceAccountProps) => {
|
||||
const [addSending, setAddSending] = useState(false);
|
||||
const [addError, setAddError] = useState("");
|
||||
const [policyDefinition, setPolicyDefinition] = useState("");
|
||||
|
||||
class AddServiceAccountContent extends React.Component<
|
||||
IAddServiceAccountContentProps,
|
||||
IAddServiceAccountContentState
|
||||
> {
|
||||
state: IAddServiceAccountContentState = {
|
||||
addLoading: false,
|
||||
addError: "",
|
||||
name: "",
|
||||
enabled: true,
|
||||
selectedPermissions: [],
|
||||
rowsPerPage: 5,
|
||||
page: 0,
|
||||
permissions: [],
|
||||
permissionsError: "",
|
||||
loadingPermissions: false,
|
||||
loadingServiceAccount: false
|
||||
useEffect(() => {
|
||||
if (addSending) {
|
||||
api
|
||||
.invoke("POST", "/api/v1/service-accounts", {
|
||||
policy: policyDefinition,
|
||||
})
|
||||
.then((res) => {
|
||||
setAddSending(false);
|
||||
setAddError("");
|
||||
closeModalAndRefresh(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
setAddSending(false);
|
||||
setAddError(err);
|
||||
});
|
||||
}
|
||||
}, [
|
||||
addSending,
|
||||
setAddSending,
|
||||
setAddError,
|
||||
policyDefinition,
|
||||
closeModalAndRefresh,
|
||||
]);
|
||||
|
||||
const addServiceAccount = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setAddSending(true);
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
// load a list of permissions
|
||||
this.setState({ loadingPermissions: true }, () => {
|
||||
api
|
||||
.invoke("GET", `/api/v1/permissions?limit=1000`)
|
||||
.then((res: PermissionList) => {
|
||||
this.setState({
|
||||
loadingPermissions: false,
|
||||
permissions: res.permissions,
|
||||
permissionsError: ""
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({ loadingPermissions: false, permissionsError: err });
|
||||
});
|
||||
});
|
||||
|
||||
const { selectedServiceAccount } = this.props;
|
||||
if (selectedServiceAccount !== null) {
|
||||
this.setState({ loadingServiceAccount: true }, () => {
|
||||
api
|
||||
.invoke(
|
||||
"GET",
|
||||
`/api/v1/service_accounts/${selectedServiceAccount.id}`
|
||||
)
|
||||
.then((res: ServiceAccountDetails) => {
|
||||
console.log(res);
|
||||
this.setState({
|
||||
loadingServiceAccount: false,
|
||||
name: selectedServiceAccount.name,
|
||||
enabled: selectedServiceAccount.enabled,
|
||||
selectedPermissions:
|
||||
res.permissions === undefined || res.permissions === null
|
||||
? []
|
||||
: res.permissions
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({ loadingServiceAccount: false });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
saveRecord(event: React.FormEvent) {
|
||||
event.preventDefault();
|
||||
const { name, addLoading, selectedPermissions, enabled } = this.state;
|
||||
const { selectedServiceAccount } = this.props;
|
||||
if (addLoading) {
|
||||
return;
|
||||
}
|
||||
this.setState({ addLoading: true }, () => {
|
||||
if (selectedServiceAccount !== null) {
|
||||
api
|
||||
.invoke(
|
||||
"PUT",
|
||||
`/api/v1/service_accounts/${selectedServiceAccount.id}`,
|
||||
{
|
||||
id: selectedServiceAccount.id,
|
||||
name: name,
|
||||
enabled: enabled,
|
||||
permission_ids: selectedPermissions.map(p => p.id)
|
||||
}
|
||||
)
|
||||
.then(res => {
|
||||
this.setState(
|
||||
{
|
||||
addLoading: false,
|
||||
addError: ""
|
||||
},
|
||||
() => {
|
||||
this.props.closeModalAndRefresh(null);
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
});
|
||||
});
|
||||
} else {
|
||||
api
|
||||
.invoke("POST", "/api/v1/service_accounts", {
|
||||
name: name,
|
||||
permission_ids: selectedPermissions.map(p => p.id)
|
||||
})
|
||||
.then((res: NewServiceAccount) => {
|
||||
this.setState(
|
||||
{
|
||||
addLoading: false,
|
||||
addError: ""
|
||||
},
|
||||
() => {
|
||||
this.props.closeModalAndRefresh(res);
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
selectedServiceAccount,
|
||||
open,
|
||||
closeModalAndRefresh
|
||||
} = this.props;
|
||||
const {
|
||||
addLoading,
|
||||
addError,
|
||||
page,
|
||||
rowsPerPage,
|
||||
permissions,
|
||||
selectedPermissions,
|
||||
name,
|
||||
loadingServiceAccount
|
||||
} = this.state;
|
||||
|
||||
const handleSelectAllClick = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (event.target.checked) {
|
||||
// const newSelecteds = permissions.map(n => n.name);
|
||||
const newSelecteds = [...permissions];
|
||||
this.setState({ selectedPermissions: newSelecteds });
|
||||
return;
|
||||
}
|
||||
this.setState({ selectedPermissions: [] });
|
||||
};
|
||||
|
||||
const handleClick = (
|
||||
event: React.MouseEvent<unknown>,
|
||||
perm: Permission
|
||||
) => {
|
||||
let newSelected: Permission[] = [...selectedPermissions];
|
||||
if (newSelected.filter(p => p.id === perm.id).length === 0) {
|
||||
newSelected.push(perm);
|
||||
} else {
|
||||
let selectedIndex = -1;
|
||||
for (let i = 0; i < newSelected.length; i++) {
|
||||
if (newSelected[i].id === perm.id) {
|
||||
selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selectedIndex >= 0) {
|
||||
newSelected = [
|
||||
...newSelected.slice(0, selectedIndex),
|
||||
...newSelected.slice(selectedIndex + 1)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ selectedPermissions: newSelected });
|
||||
};
|
||||
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
this.setState({ page: newPage });
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
this.setState({ page: 0, rowsPerPage: parseInt(event.target.value, 10) });
|
||||
};
|
||||
|
||||
const isSelected = (perm: Permission) =>
|
||||
selectedPermissions.filter(p => p.id === perm.id).length > 0;
|
||||
|
||||
const emptyRows =
|
||||
rowsPerPage -
|
||||
Math.min(rowsPerPage, permissions.length - page * rowsPerPage);
|
||||
|
||||
const handleChange = (name: string) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
this.setState({ enabled: event.target.checked });
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalWrapper
|
||||
modalOpen={open}
|
||||
onClose={() => {
|
||||
this.setState({ addError: "" }, () => {
|
||||
closeModalAndRefresh(null);
|
||||
});
|
||||
return (
|
||||
<ModalWrapper
|
||||
modalOpen={open}
|
||||
onClose={() => {
|
||||
closeModalAndRefresh(null);
|
||||
}}
|
||||
title={`Create Service Account`}
|
||||
>
|
||||
<form
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
||||
addServiceAccount(e);
|
||||
}}
|
||||
title={
|
||||
selectedServiceAccount !== null
|
||||
? "Edit Service Account"
|
||||
: "Create Service Account"
|
||||
}
|
||||
>
|
||||
<form
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
||||
this.saveRecord(e);
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
{loadingServiceAccount && (
|
||||
<Grid item xs={12}>
|
||||
<LinearProgress />
|
||||
</Grid>
|
||||
)}
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
@@ -383,155 +121,46 @@ class AddServiceAccountContent extends React.Component<
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="service-account-name"
|
||||
name="service-account-name"
|
||||
label="Name"
|
||||
value={name}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ name: e.target.value });
|
||||
<Typography component="h5">
|
||||
Optional Policy
|
||||
<Tooltip
|
||||
title="A policy that restricts this service account can be attached."
|
||||
placement="top-start"
|
||||
>
|
||||
<HelpIcon />
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
<CodeMirror
|
||||
className={classes.codeMirror}
|
||||
options={{
|
||||
mode: "javascript",
|
||||
lineNumbers: true,
|
||||
}}
|
||||
onChange={(editor, data, value) => {
|
||||
setPolicyDefinition(value);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<div className={classes.root}>
|
||||
<EnhancedTableToolbar
|
||||
numSelected={selectedPermissions.length}
|
||||
/>
|
||||
<TableContainer>
|
||||
<Table
|
||||
className={classes.table}
|
||||
aria-labelledby="tableTitle"
|
||||
size={"small"}
|
||||
aria-label="enhanced table"
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
indeterminate={
|
||||
selectedPermissions.length > 0 &&
|
||||
selectedPermissions.length < permissions.length
|
||||
}
|
||||
checked={
|
||||
selectedPermissions.length > 0 &&
|
||||
selectedPermissions.length === permissions.length
|
||||
}
|
||||
onChange={handleSelectAllClick}
|
||||
inputProps={{
|
||||
"aria-label": "select all desserts"
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>Permission</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{permissions
|
||||
.slice(
|
||||
page * rowsPerPage,
|
||||
page * rowsPerPage + rowsPerPage
|
||||
)
|
||||
.map((row, index) => {
|
||||
const isItemSelected = isSelected(row);
|
||||
const labelId = `enhanced-table-checkbox-${index}`;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
hover
|
||||
onClick={event => handleClick(event, row)}
|
||||
role="checkbox"
|
||||
aria-checked={isItemSelected}
|
||||
tabIndex={-1}
|
||||
key={row.name}
|
||||
selected={isItemSelected}
|
||||
>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
checked={isItemSelected}
|
||||
inputProps={{ "aria-labelledby": labelId }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell id={labelId}>{row.name}</TableCell>
|
||||
<TableCell>{row.description}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow style={{ height: 33 * emptyRows }}>
|
||||
<TableCell colSpan={6} />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
component="div"
|
||||
count={permissions.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onChangePage={handleChangePage}
|
||||
onChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch onChange={handleChange("enabled")} value="checkedA" />
|
||||
}
|
||||
label="Enabled"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={addLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Grid>
|
||||
{addLoading && (
|
||||
<Grid item xs={12}>
|
||||
<LinearProgress />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</form>
|
||||
</ModalWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={addSending}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</Grid>
|
||||
{addSending && (
|
||||
<Grid item xs={12}>
|
||||
<LinearProgress />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</form>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const AddServiceAccountWrapper = withStyles(styles)(AddServiceAccountContent);
|
||||
|
||||
interface IAddServiceAccountProps {
|
||||
open: boolean;
|
||||
closeModalAndRefresh: (res: NewServiceAccount | null) => void;
|
||||
selectedServiceAccount: ServiceAccount | null;
|
||||
}
|
||||
|
||||
interface IAddServiceAccountState {}
|
||||
|
||||
class AddServiceAccount extends React.Component<
|
||||
IAddServiceAccountProps,
|
||||
IAddServiceAccountState
|
||||
> {
|
||||
state: IAddServiceAccountState = {};
|
||||
|
||||
render() {
|
||||
return <AddServiceAccountWrapper {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default AddServiceAccount;
|
||||
export default withStyles(styles)(AddServiceAccount);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2019 MinIO, Inc.
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -17,20 +17,19 @@
|
||||
import React from "react";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { NewServiceAccount } from "./types";
|
||||
import {
|
||||
Button,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText
|
||||
} from "@material-ui/core";
|
||||
import { Button } from "@material-ui/core";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
}
|
||||
color: "red",
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right",
|
||||
},
|
||||
});
|
||||
|
||||
interface ICredentialsPromptProps {
|
||||
@@ -40,76 +39,69 @@ interface ICredentialsPromptProps {
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
interface ICredentialsPromptState {}
|
||||
const download = (filename: string, text: string) => {
|
||||
let element = document.createElement("a");
|
||||
element.setAttribute(
|
||||
"href",
|
||||
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
|
||||
);
|
||||
element.setAttribute("download", filename);
|
||||
|
||||
class CredentialsPrompt extends React.Component<
|
||||
ICredentialsPromptProps,
|
||||
ICredentialsPromptState
|
||||
> {
|
||||
state: ICredentialsPromptState = {};
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
|
||||
download(filename: string, text: string) {
|
||||
var element = document.createElement("a");
|
||||
element.setAttribute(
|
||||
"href",
|
||||
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
|
||||
);
|
||||
element.setAttribute("download", filename);
|
||||
element.click();
|
||||
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
document.body.removeChild(element);
|
||||
};
|
||||
|
||||
element.click();
|
||||
|
||||
document.body.removeChild(element);
|
||||
const CredentialsPrompt = ({
|
||||
classes,
|
||||
newServiceAccount,
|
||||
open,
|
||||
closeModal,
|
||||
}: ICredentialsPromptProps) => {
|
||||
if (!newServiceAccount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes, open, newServiceAccount } = this.props;
|
||||
|
||||
if (newServiceAccount === null) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalWrapper
|
||||
modalOpen={open}
|
||||
onClose={() => {
|
||||
this.props.closeModal();
|
||||
}}
|
||||
title="New Service Account"
|
||||
>
|
||||
<React.Fragment>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
A new service account has been created with the following details:
|
||||
<ul>
|
||||
<li>
|
||||
<b>Access Key:</b>{" "}
|
||||
{newServiceAccount.service_account.access_key}
|
||||
</li>
|
||||
<li>
|
||||
<b>Secret Key:</b> {newServiceAccount.secret_key}
|
||||
</li>
|
||||
</ul>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
Write these down, as this is the only time the secret will be
|
||||
displayed.
|
||||
</Typography>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
return (
|
||||
<ModalWrapper
|
||||
modalOpen={open}
|
||||
onClose={() => {
|
||||
closeModal();
|
||||
}}
|
||||
title="New Service Account Created"
|
||||
>
|
||||
<React.Fragment>
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
A new service account has been created with the following details:
|
||||
<ul>
|
||||
<li>
|
||||
<b>Access Key:</b> {newServiceAccount.accessKey}
|
||||
</li>
|
||||
<li>
|
||||
<b>Secret Key:</b> {newServiceAccount.secretKey}
|
||||
</li>
|
||||
</ul>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
Write these down, as this is the only time the secret will be
|
||||
displayed.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.download(
|
||||
download(
|
||||
"credentials.json",
|
||||
JSON.stringify({
|
||||
access_key: newServiceAccount.service_account.access_key,
|
||||
secret_key: newServiceAccount.secret_key
|
||||
access_key: newServiceAccount.accessKey,
|
||||
secret_key: newServiceAccount.secretKey,
|
||||
})
|
||||
);
|
||||
}}
|
||||
@@ -119,18 +111,18 @@ class CredentialsPrompt extends React.Component<
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.props.closeModal();
|
||||
closeModal();
|
||||
}}
|
||||
color="secondary"
|
||||
autoFocus
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
</ModalWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(CredentialsPrompt);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2019 MinIO, Inc.
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -14,152 +14,117 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import React from "react";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
LinearProgress
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
LinearProgress,
|
||||
} from "@material-ui/core";
|
||||
import api from "../../../common/api";
|
||||
import { ServiceAccount, ServiceAccountsList } from "./types";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
},
|
||||
wrapText: {
|
||||
maxWidth: "200px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word"
|
||||
}
|
||||
});
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red",
|
||||
},
|
||||
wrapText: {
|
||||
maxWidth: "200px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word",
|
||||
},
|
||||
});
|
||||
|
||||
interface IDeleteServiceAccountProps {
|
||||
classes: any;
|
||||
closeDeleteModalAndRefresh: (refresh: boolean) => void;
|
||||
deleteOpen: boolean;
|
||||
selectedServiceAccount: ServiceAccount | null;
|
||||
classes: any;
|
||||
closeDeleteModalAndRefresh: (refresh: boolean) => void;
|
||||
deleteOpen: boolean;
|
||||
selectedServiceAccount: string | null;
|
||||
}
|
||||
|
||||
interface IDeleteServiceAccountState {
|
||||
deleteLoading: boolean;
|
||||
deleteError: string;
|
||||
}
|
||||
const DeleteServiceAccount = ({
|
||||
classes,
|
||||
closeDeleteModalAndRefresh,
|
||||
deleteOpen,
|
||||
selectedServiceAccount,
|
||||
}: IDeleteServiceAccountProps) => {
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [deleteError, setDeleteError] = useState("");
|
||||
|
||||
class DeleteServiceAccount extends React.Component<
|
||||
IDeleteServiceAccountProps,
|
||||
IDeleteServiceAccountState
|
||||
> {
|
||||
state: IDeleteServiceAccountState = {
|
||||
deleteLoading: false,
|
||||
deleteError: ""
|
||||
};
|
||||
|
||||
removeRecord() {
|
||||
const { deleteLoading } = this.state;
|
||||
const { selectedServiceAccount } = this.props;
|
||||
if (deleteLoading) {
|
||||
return;
|
||||
}
|
||||
if (selectedServiceAccount == null) {
|
||||
return;
|
||||
}
|
||||
this.setState({ deleteLoading: true }, () => {
|
||||
api
|
||||
.invoke("DELETE", `/api/v1/service_accounts/${selectedServiceAccount.id}`, {
|
||||
id: selectedServiceAccount.id
|
||||
})
|
||||
.then((res: ServiceAccountsList) => {
|
||||
this.setState(
|
||||
{
|
||||
deleteLoading: false,
|
||||
deleteError: ""
|
||||
},
|
||||
() => {
|
||||
this.props.closeDeleteModalAndRefresh(true);
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({
|
||||
deleteLoading: false,
|
||||
deleteError: err
|
||||
});
|
||||
});
|
||||
useEffect(() => {
|
||||
if (deleteLoading) {
|
||||
api
|
||||
.invoke("DELETE", `/api/v1/service-accounts/${selectedServiceAccount}`)
|
||||
.then(() => {
|
||||
setDeleteLoading(false);
|
||||
setDeleteError("");
|
||||
closeDeleteModalAndRefresh(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
setDeleteLoading(false);
|
||||
setDeleteError(err);
|
||||
});
|
||||
}
|
||||
}, [deleteLoading, closeDeleteModalAndRefresh, selectedServiceAccount]);
|
||||
|
||||
render() {
|
||||
const { classes, deleteOpen, selectedServiceAccount } = this.props;
|
||||
const { deleteLoading, deleteError } = this.state;
|
||||
|
||||
if (selectedServiceAccount === null) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={deleteOpen}
|
||||
onClose={() => {
|
||||
this.setState({ deleteError: "" }, () => {
|
||||
this.props.closeDeleteModalAndRefresh(false);
|
||||
});
|
||||
}}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">Delete ServiceAccount</DialogTitle>
|
||||
<DialogContent>
|
||||
{deleteLoading && <LinearProgress />}
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Are you sure you want to delete service account{" "}
|
||||
<b className={classes.wrapText}>{selectedServiceAccount.name}</b>?
|
||||
{deleteError !== "" && (
|
||||
<React.Fragment>
|
||||
<br />
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{deleteError}
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.setState({ deleteError: "" }, () => {
|
||||
this.props.closeDeleteModalAndRefresh(false);
|
||||
});
|
||||
}}
|
||||
color="primary"
|
||||
disabled={deleteLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.removeRecord();
|
||||
}}
|
||||
color="secondary"
|
||||
autoFocus
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
const removeRecord = () => {
|
||||
if (selectedServiceAccount == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setDeleteLoading(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={deleteOpen}
|
||||
onClose={() => {
|
||||
closeDeleteModalAndRefresh(false);
|
||||
}}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">Delete ServiceAccount</DialogTitle>
|
||||
<DialogContent>
|
||||
{deleteLoading && <LinearProgress />}
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Are you sure you want to delete service account{" "}
|
||||
<b className={classes.wrapText}>{selectedServiceAccount}</b>?
|
||||
{deleteError !== "" && (
|
||||
<React.Fragment>
|
||||
<br />
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{deleteError}
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
closeDeleteModalAndRefresh(false);
|
||||
}}
|
||||
color="primary"
|
||||
disabled={deleteLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={removeRecord} color="secondary" autoFocus>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(DeleteServiceAccount);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2019 MinIO, Inc.
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -14,401 +14,295 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import Table from "@material-ui/core/Table";
|
||||
import TableBody from "@material-ui/core/TableBody";
|
||||
import TableCell from "@material-ui/core/TableCell";
|
||||
import TableHead from "@material-ui/core/TableHead";
|
||||
import TableRow from "@material-ui/core/TableRow";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import api from "../../../common/api";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
TableFooter,
|
||||
TablePagination
|
||||
} from "@material-ui/core";
|
||||
import { Button } from "@material-ui/core";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import {
|
||||
NewServiceAccount,
|
||||
ServiceAccount,
|
||||
ServiceAccountsList
|
||||
} from "./types";
|
||||
import { NewServiceAccount } from "./types";
|
||||
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import AddServiceAccount from "./AddServiceAccount";
|
||||
import DeleteServiceAccount from "./DeleteServiceAccount";
|
||||
import CredentialsPrompt from "./CredentialsPrompt";
|
||||
import { CreateIcon, DeleteIcon } from "../../../icons";
|
||||
import Checkbox from "@material-ui/core/Checkbox";
|
||||
import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded";
|
||||
import { CreateIcon } from "../../../icons";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import InputAdornment from "@material-ui/core/InputAdornment";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import TableWrapper from "../Common/TableWrapper/TableWrapper";
|
||||
import { stringSort } from "../../../utils/sortFunctions";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
seeMore: {
|
||||
marginTop: theme.spacing(3)
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
paper: {
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column"
|
||||
flexDirection: "column",
|
||||
},
|
||||
addSideBar: {
|
||||
width: "480px",
|
||||
minWidth: "320px",
|
||||
padding: "20px"
|
||||
padding: "20px",
|
||||
},
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
tableToolbar: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(0)
|
||||
paddingRight: theme.spacing(0),
|
||||
},
|
||||
wrapCell: {
|
||||
maxWidth: "200px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word"
|
||||
wordWrap: "break-word",
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
"& tr": {
|
||||
"& th": {
|
||||
fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
imageIcon: {
|
||||
height: "100%"
|
||||
height: "100%",
|
||||
},
|
||||
iconRoot: {
|
||||
textAlign: "center"
|
||||
textAlign: "center",
|
||||
},
|
||||
actionsTray: {
|
||||
textAlign: "right",
|
||||
"& button": {
|
||||
marginLeft: 10
|
||||
}
|
||||
marginLeft: 10,
|
||||
},
|
||||
},
|
||||
searchField: {
|
||||
background: "#FFFFFF",
|
||||
padding: 12,
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 3px 6px #00000012"
|
||||
}
|
||||
boxShadow: "0px 3px 6px #00000012",
|
||||
},
|
||||
});
|
||||
|
||||
interface IServiceAccountsProps {
|
||||
classes: any;
|
||||
}
|
||||
|
||||
interface IServiceAccountsState {
|
||||
records: ServiceAccount[];
|
||||
totalRecords: number;
|
||||
loading: boolean;
|
||||
error: string;
|
||||
deleteError: string;
|
||||
addScreenOpen: boolean;
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
deleteOpen: boolean;
|
||||
selectedServiceAccount: ServiceAccount | null;
|
||||
showNewCredentials: boolean;
|
||||
newServiceAccount: NewServiceAccount | null;
|
||||
}
|
||||
const ServiceAccounts = ({ classes }: IServiceAccountsProps) => {
|
||||
const [records, setRecords] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>("");
|
||||
const [filter, setFilter] = useState<string>("");
|
||||
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
|
||||
const [page, setPage] = useState<number>(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
|
||||
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
|
||||
const [selectedServiceAccount, setSelectedServiceAccount] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [showNewCredentials, setShowNewCredentials] = useState<boolean>(false);
|
||||
const [
|
||||
newServiceAccount,
|
||||
setNewServiceAccount,
|
||||
] = useState<NewServiceAccount | null>(null);
|
||||
|
||||
class ServiceAccounts extends React.Component<
|
||||
IServiceAccountsProps,
|
||||
IServiceAccountsState
|
||||
> {
|
||||
state: IServiceAccountsState = {
|
||||
records: [],
|
||||
totalRecords: 0,
|
||||
loading: false,
|
||||
error: "",
|
||||
deleteError: "",
|
||||
addScreenOpen: false,
|
||||
page: 0,
|
||||
rowsPerPage: 10,
|
||||
deleteOpen: false,
|
||||
selectedServiceAccount: null,
|
||||
showNewCredentials: false,
|
||||
newServiceAccount: null
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchRecords();
|
||||
}, []);
|
||||
|
||||
fetchRecords() {
|
||||
this.setState({ loading: true }, () => {
|
||||
const { page, rowsPerPage } = this.state;
|
||||
const offset = page * rowsPerPage;
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
api
|
||||
.invoke(
|
||||
"GET",
|
||||
`/api/v1/service_accounts?offset=${offset}&limit=${rowsPerPage}`
|
||||
)
|
||||
.then((res: ServiceAccountsList) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
records: res.service_accounts,
|
||||
totalRecords: res.total,
|
||||
error: ""
|
||||
});
|
||||
.invoke("GET", `/api/v1/service-accounts`)
|
||||
.then((res: string[]) => {
|
||||
const serviceAccounts = res.sort(stringSort);
|
||||
|
||||
setLoading(false);
|
||||
setRecords(serviceAccounts);
|
||||
setError("");
|
||||
|
||||
// if we get 0 results, and page > 0 , go down 1 page
|
||||
if (
|
||||
(res.service_accounts === undefined ||
|
||||
res.service_accounts == null ||
|
||||
res.service_accounts.length === 0) &&
|
||||
page > 0
|
||||
) {
|
||||
if ((!serviceAccounts || serviceAccounts.length === 0) && page > 0) {
|
||||
const newPage = page - 1;
|
||||
this.setState({ page: newPage }, () => {
|
||||
this.fetchRecords();
|
||||
});
|
||||
setPage(newPage);
|
||||
fetchRecords();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({ loading: false, error: err });
|
||||
.catch((err) => {
|
||||
setError(err);
|
||||
setLoading(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
closeAddModalAndRefresh(res: NewServiceAccount | null) {
|
||||
this.setState({ addScreenOpen: false }, () => {
|
||||
this.fetchRecords();
|
||||
});
|
||||
if (res !== null) {
|
||||
this.setState({ showNewCredentials: true, newServiceAccount: res });
|
||||
}
|
||||
}
|
||||
}, [loading, setLoading, setRecords, setError, page, setPage]);
|
||||
|
||||
closeDeleteModalAndRefresh(refresh: boolean) {
|
||||
this.setState({ deleteOpen: false }, () => {
|
||||
if (refresh) {
|
||||
this.fetchRecords();
|
||||
}
|
||||
});
|
||||
}
|
||||
const fetchRecords = () => {
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
this.fetchRecords();
|
||||
}
|
||||
const closeAddModalAndRefresh = (res: NewServiceAccount | null) => {
|
||||
setAddScreenOpen(false);
|
||||
fetchRecords();
|
||||
|
||||
closeCredentialsModal() {
|
||||
this.setState({ showNewCredentials: false, newServiceAccount: null });
|
||||
}
|
||||
if (res !== null) {
|
||||
setShowNewCredentials(true);
|
||||
setNewServiceAccount(res);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const {
|
||||
records,
|
||||
totalRecords,
|
||||
addScreenOpen,
|
||||
loading,
|
||||
page,
|
||||
rowsPerPage,
|
||||
deleteOpen,
|
||||
selectedServiceAccount,
|
||||
showNewCredentials,
|
||||
newServiceAccount
|
||||
} = this.state;
|
||||
const closeDeleteModalAndRefresh = (refresh: boolean) => {
|
||||
setDeleteOpen(false);
|
||||
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
this.setState({ page: newPage }, () => {
|
||||
this.fetchRecords();
|
||||
});
|
||||
};
|
||||
if (refresh) {
|
||||
fetchRecords();
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const rPP = parseInt(event.target.value, 10);
|
||||
this.setState({ page: 0, rowsPerPage: rPP }, () => {
|
||||
this.fetchRecords();
|
||||
});
|
||||
};
|
||||
const closeCredentialsModal = () => {
|
||||
setShowNewCredentials(false);
|
||||
setNewServiceAccount(null);
|
||||
};
|
||||
|
||||
const confirmDeleteServiceAccount = (
|
||||
selectedServiceAccount: ServiceAccount
|
||||
) => {
|
||||
this.setState({
|
||||
deleteOpen: true,
|
||||
selectedServiceAccount: selectedServiceAccount
|
||||
});
|
||||
};
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const editServiceAccount = (selectedServiceAccount: ServiceAccount) => {
|
||||
this.setState({
|
||||
addScreenOpen: true,
|
||||
selectedServiceAccount: selectedServiceAccount
|
||||
});
|
||||
};
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const rPP = parseInt(event.target.value, 10);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{addScreenOpen && (
|
||||
<AddServiceAccount
|
||||
open={addScreenOpen}
|
||||
selectedServiceAccount={selectedServiceAccount}
|
||||
closeModalAndRefresh={(res: NewServiceAccount | null) => {
|
||||
this.closeAddModalAndRefresh(res);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{deleteOpen && (
|
||||
<DeleteServiceAccount
|
||||
deleteOpen={deleteOpen}
|
||||
selectedServiceAccount={selectedServiceAccount}
|
||||
closeDeleteModalAndRefresh={(refresh: boolean) => {
|
||||
this.closeDeleteModalAndRefresh(refresh);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showNewCredentials && (
|
||||
<CredentialsPrompt
|
||||
newServiceAccount={newServiceAccount}
|
||||
open={showNewCredentials}
|
||||
closeModal={() => {
|
||||
this.closeCredentialsModal();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Service Accounts</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.actionsTray}>
|
||||
<TextField
|
||||
placeholder="Search Service Accounts"
|
||||
className={classes.searchField}
|
||||
id="search-resource"
|
||||
label=""
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<CreateIcon />}
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
addScreenOpen: true,
|
||||
selectedServiceAccount: null
|
||||
});
|
||||
}}
|
||||
>
|
||||
Create service account
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<PlayArrowRoundedIcon />}
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
addScreenOpen: true
|
||||
});
|
||||
}}
|
||||
>
|
||||
Change Access
|
||||
</Button>
|
||||
</Grid>
|
||||
setPage(0);
|
||||
setRowsPerPage(rPP);
|
||||
};
|
||||
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Paper className={classes.paper}>
|
||||
{loading && <LinearProgress />}
|
||||
{records != null && records.length > 0 ? (
|
||||
<Table size="medium">
|
||||
<TableHead className={classes.minTableHeader}>
|
||||
<TableRow>
|
||||
<TableCell>Select</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Access Key</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{records.map(row => (
|
||||
<TableRow key={row.name}>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
value="secondary"
|
||||
color="primary"
|
||||
inputProps={{ "aria-label": "secondary checkbox" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.wrapCell}>
|
||||
{row.name}
|
||||
</TableCell>
|
||||
<TableCell>{row.access_key}</TableCell>
|
||||
<TableCell>
|
||||
{row.enabled ? "enabled" : "disabled"}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
aria-label="edit"
|
||||
onClick={() => {
|
||||
editServiceAccount(row);
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="delete"
|
||||
onClick={() => {
|
||||
confirmDeleteServiceAccount(row);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
colSpan={4}
|
||||
count={totalRecords}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
SelectProps={{
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true
|
||||
}}
|
||||
onChangePage={handleChangePage}
|
||||
onChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
ActionsComponent={MinTablePaginationActions}
|
||||
/>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
) : (
|
||||
<div>No Service Accounts</div>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
const confirmDeleteServiceAccount = (selectedServiceAccount: string) => {
|
||||
setSelectedServiceAccount(selectedServiceAccount);
|
||||
setDeleteOpen(true);
|
||||
};
|
||||
|
||||
const tableActions = [
|
||||
{ type: "delete", onClick: confirmDeleteServiceAccount },
|
||||
];
|
||||
|
||||
const filteredRecords = records.filter((elementItem) =>
|
||||
elementItem.toLowerCase().includes(filter.toLowerCase())
|
||||
);
|
||||
|
||||
const beginRecord = page * rowsPerPage;
|
||||
const endRecords = beginRecord + rowsPerPage;
|
||||
|
||||
const paginatedRecords = filteredRecords.slice(beginRecord, endRecords);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{addScreenOpen && (
|
||||
<AddServiceAccount
|
||||
open={addScreenOpen}
|
||||
closeModalAndRefresh={(res: NewServiceAccount | null) => {
|
||||
closeAddModalAndRefresh(res);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{deleteOpen && (
|
||||
<DeleteServiceAccount
|
||||
deleteOpen={deleteOpen}
|
||||
selectedServiceAccount={selectedServiceAccount}
|
||||
closeDeleteModalAndRefresh={(refresh: boolean) => {
|
||||
closeDeleteModalAndRefresh(refresh);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showNewCredentials && (
|
||||
<CredentialsPrompt
|
||||
newServiceAccount={newServiceAccount}
|
||||
open={showNewCredentials}
|
||||
closeModal={() => {
|
||||
closeCredentialsModal();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Service Accounts</Typography>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.actionsTray}>
|
||||
<TextField
|
||||
placeholder="Search Service Accounts"
|
||||
className={classes.searchField}
|
||||
id="search-resource"
|
||||
label=""
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setFilter(e.target.value);
|
||||
setPage(0);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<CreateIcon />}
|
||||
onClick={() => {
|
||||
setAddScreenOpen(true);
|
||||
setSelectedServiceAccount(null);
|
||||
}}
|
||||
>
|
||||
Create service account
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
{error !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<TableWrapper
|
||||
isLoading={loading}
|
||||
records={paginatedRecords}
|
||||
entityName={"Service Accounts"}
|
||||
idField={""}
|
||||
columns={[{ label: "Service Account", elementKey: "" }]}
|
||||
itemActions={tableActions}
|
||||
paginatorConfig={{
|
||||
rowsPerPageOptions: [5, 10, 25],
|
||||
colSpan: 4,
|
||||
count: records.length,
|
||||
rowsPerPage: rowsPerPage,
|
||||
page,
|
||||
SelectProps: {
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true,
|
||||
},
|
||||
onChangePage: handleChangePage,
|
||||
onChangeRowsPerPage: handleChangeRowsPerPage,
|
||||
ActionsComponent: MinTablePaginationActions,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(ServiceAccounts);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2019 MinIO, Inc.
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -14,27 +14,12 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import {Permission} from "../Permissions/types";
|
||||
|
||||
export interface ServiceAccount {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
access_key: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface ServiceAccountsList {
|
||||
service_accounts: ServiceAccount[];
|
||||
total:number;
|
||||
service_accounts: string[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface NewServiceAccount {
|
||||
service_account: ServiceAccount,
|
||||
secret_key:string,
|
||||
}
|
||||
|
||||
export interface ServiceAccountDetails {
|
||||
service_account: ServiceAccount,
|
||||
permissions: Permission[],
|
||||
accessKey: string;
|
||||
secretKey: string;
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React, { useEffect } from "react";
|
||||
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
|
||||
import storage from "local-storage-fallback";
|
||||
import { AppState } from "../../../store";
|
||||
import { connect } from "react-redux";
|
||||
import { traceMessageReceived, traceResetMessages } from "./actions";
|
||||
import { TraceMessage } from "./types";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { niceBytes, setCookie } from "../../../common/utils";
|
||||
import { niceBytes, timeFromDate } from "../../../common/utils";
|
||||
import { wsProtocol } from "../../../utils/wsUtils";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -31,15 +31,15 @@ const styles = (theme: Theme) =>
|
||||
overflow: "auto",
|
||||
"& ul": {
|
||||
margin: "4px",
|
||||
padding: "0px"
|
||||
padding: "0px",
|
||||
},
|
||||
"& ul li": {
|
||||
listStyle: "none",
|
||||
margin: "0px",
|
||||
padding: "0px",
|
||||
borderBottom: "1px solid #dedede"
|
||||
}
|
||||
}
|
||||
borderBottom: "1px solid #dedede",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface ITrace {
|
||||
@@ -53,18 +53,16 @@ const Trace = ({
|
||||
classes,
|
||||
traceMessageReceived,
|
||||
traceResetMessages,
|
||||
messages
|
||||
messages,
|
||||
}: ITrace) => {
|
||||
useEffect(() => {
|
||||
traceResetMessages();
|
||||
const token: string = storage.getItem("token")!;
|
||||
const url = new URL(window.location.toString());
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
const port = isDev ? "9090" : url.port;
|
||||
|
||||
setCookie("token", token);
|
||||
|
||||
const c = new W3CWebSocket(`ws://${url.hostname}:${port}/ws/trace`);
|
||||
const wsProt = wsProtocol(url.protocol);
|
||||
const c = new W3CWebSocket(`${wsProt}://${url.hostname}:${port}/ws/trace`);
|
||||
|
||||
let interval: any | null = null;
|
||||
if (c !== null) {
|
||||
@@ -93,23 +91,15 @@ const Trace = ({
|
||||
}
|
||||
}, [traceMessageReceived]);
|
||||
|
||||
const timeFromdate = (d: Date) => {
|
||||
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
|
||||
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;
|
||||
let s = d.getSeconds() < 10 ? `0${d.getSeconds()}` : `${d.getSeconds()}`;
|
||||
|
||||
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Trace</h1>
|
||||
<div className={classes.logList}>
|
||||
<ul>
|
||||
{messages.map(m => {
|
||||
{messages.map((m) => {
|
||||
return (
|
||||
<li key={m.key}>
|
||||
{timeFromdate(m.time)} - {m.api}[{m.statusCode} {m.statusMsg}]{" "}
|
||||
{timeFromDate(m.time)} - {m.api}[{m.statusCode} {m.statusMsg}]{" "}
|
||||
{m.api} {m.host} {m.client} {m.callStats.duration} ↑{" "}
|
||||
{niceBytes(m.callStats.rx + "")} ↓{" "}
|
||||
{niceBytes(m.callStats.tx + "")}
|
||||
@@ -123,12 +113,12 @@ const Trace = ({
|
||||
};
|
||||
|
||||
const mapState = (state: AppState) => ({
|
||||
messages: state.trace.messages
|
||||
messages: state.trace.messages,
|
||||
});
|
||||
|
||||
const connector = connect(mapState, {
|
||||
traceMessageReceived: traceMessageReceived,
|
||||
traceResetMessages: traceResetMessages
|
||||
traceResetMessages: traceResetMessages,
|
||||
});
|
||||
|
||||
export default connector(withStyles(styles)(Trace));
|
||||
|
||||
@@ -35,12 +35,12 @@ export type TraceActionTypes =
|
||||
export function traceMessageReceived(message: TraceMessage) {
|
||||
return {
|
||||
type: TRACE_MESSAGE_RECEIVED,
|
||||
message: message
|
||||
message: message,
|
||||
};
|
||||
}
|
||||
|
||||
export function traceResetMessages() {
|
||||
return {
|
||||
type: TRACE_RESET_MESSAGES
|
||||
type: TRACE_RESET_MESSAGES,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import {
|
||||
TRACE_MESSAGE_RECEIVED,
|
||||
TRACE_RESET_MESSAGES,
|
||||
TraceActionTypes
|
||||
TraceActionTypes,
|
||||
} from "./actions";
|
||||
import { TraceMessage } from "./types";
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface TraceState {
|
||||
}
|
||||
|
||||
const initialState: TraceState = {
|
||||
messages: []
|
||||
messages: [],
|
||||
};
|
||||
|
||||
export function traceReducer(
|
||||
@@ -37,12 +37,12 @@ export function traceReducer(
|
||||
case TRACE_MESSAGE_RECEIVED:
|
||||
return {
|
||||
...state,
|
||||
messages: [...state.messages, action.message]
|
||||
messages: [...state.messages, action.message],
|
||||
};
|
||||
case TRACE_RESET_MESSAGES:
|
||||
return {
|
||||
...state,
|
||||
messages: []
|
||||
messages: [],
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
|
||||
@@ -18,6 +18,7 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { Button, LinearProgress } from "@material-ui/core";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
|
||||
import api from "../../../common/api";
|
||||
import GroupsSelectors from "./GroupsSelectors";
|
||||
import Title from "../../../common/Title";
|
||||
@@ -33,24 +34,25 @@ interface IAddToGroup {
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
strongText: {
|
||||
fontWeight: 700
|
||||
fontWeight: 700,
|
||||
},
|
||||
keyName: {
|
||||
marginLeft: 5
|
||||
marginLeft: 5,
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right"
|
||||
}
|
||||
textAlign: "right",
|
||||
},
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
const AddToGroup = ({
|
||||
open,
|
||||
checkedUsers,
|
||||
closeModalAndRefresh,
|
||||
classes
|
||||
classes,
|
||||
}: IAddToGroup) => {
|
||||
//Local States
|
||||
const [saving, isSaving] = useState<boolean>(false);
|
||||
@@ -64,14 +66,14 @@ const AddToGroup = ({
|
||||
api
|
||||
.invoke("PUT", "/api/v1/users-groups-bulk", {
|
||||
groups: selectedGroups,
|
||||
users: checkedUsers
|
||||
users: checkedUsers,
|
||||
})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
isSaving(false);
|
||||
setError("");
|
||||
closeModalAndRefresh(true);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
isSaving(false);
|
||||
setError(err);
|
||||
});
|
||||
@@ -86,7 +88,7 @@ const AddToGroup = ({
|
||||
setError,
|
||||
closeModalAndRefresh,
|
||||
selectedGroups,
|
||||
checkedUsers
|
||||
checkedUsers,
|
||||
]);
|
||||
|
||||
//Fetch Actions
|
||||
@@ -106,35 +108,34 @@ const AddToGroup = ({
|
||||
>
|
||||
<form noValidate autoComplete="off" onSubmit={setSaving}>
|
||||
<Grid container>
|
||||
{updatingError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{updatingError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
{updatingError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{updatingError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Title>Users to be altered</Title>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{checkedUsers.join(", ")}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<GroupsSelectors
|
||||
selectedGroups={selectedGroups}
|
||||
setSelectedGroups={setSelectedGroups}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
<Grid item xs={12}>
|
||||
<Title>Users to be altered</Title>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{checkedUsers.join(", ")}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<GroupsSelectors
|
||||
selectedGroups={selectedGroups}
|
||||
setSelectedGroups={setSelectedGroups}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
|
||||
@@ -19,8 +19,9 @@ import Grid from "@material-ui/core/Grid";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { Button, LinearProgress } from "@material-ui/core";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import api from "../../../common/api";
|
||||
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
|
||||
import { User } from "./types";
|
||||
import api from "../../../common/api";
|
||||
import GroupsSelectors from "./GroupsSelectors";
|
||||
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
@@ -29,17 +30,18 @@ import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/Radi
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
strongText: {
|
||||
fontWeight: 700
|
||||
fontWeight: 700,
|
||||
},
|
||||
keyName: {
|
||||
marginLeft: 5
|
||||
marginLeft: 5,
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right"
|
||||
}
|
||||
textAlign: "right",
|
||||
},
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
interface IAddUserContentProps {
|
||||
@@ -68,7 +70,7 @@ class AddUserContent extends React.Component<
|
||||
accessKey: "",
|
||||
secretKey: "",
|
||||
enabled: "enabled",
|
||||
selectedGroups: []
|
||||
selectedGroups: [],
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -77,7 +79,7 @@ class AddUserContent extends React.Component<
|
||||
this.setState({
|
||||
accessKey: "",
|
||||
secretKey: "",
|
||||
selectedGroups: []
|
||||
selectedGroups: [],
|
||||
});
|
||||
} else {
|
||||
this.getUserInformation();
|
||||
@@ -91,7 +93,7 @@ class AddUserContent extends React.Component<
|
||||
addLoading,
|
||||
secretKey,
|
||||
selectedGroups,
|
||||
enabled
|
||||
enabled,
|
||||
} = this.state;
|
||||
const { selectedUser } = this.props;
|
||||
if (addLoading) {
|
||||
@@ -102,23 +104,23 @@ class AddUserContent extends React.Component<
|
||||
api
|
||||
.invoke("PUT", `/api/v1/users/${selectedUser.accessKey}`, {
|
||||
status: enabled,
|
||||
groups: selectedGroups
|
||||
groups: selectedGroups,
|
||||
})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
this.setState(
|
||||
{
|
||||
addLoading: false,
|
||||
addError: ""
|
||||
addError: "",
|
||||
},
|
||||
() => {
|
||||
this.props.closeModalAndRefresh();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
addError: err,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@@ -126,24 +128,24 @@ class AddUserContent extends React.Component<
|
||||
.invoke("POST", "/api/v1/users", {
|
||||
accessKey,
|
||||
secretKey,
|
||||
groups: selectedGroups
|
||||
groups: selectedGroups,
|
||||
})
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
this.setState(
|
||||
{
|
||||
addLoading: false,
|
||||
addError: ""
|
||||
addError: "",
|
||||
},
|
||||
() => {
|
||||
this.props.closeModalAndRefresh();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
addError: err,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -159,19 +161,19 @@ class AddUserContent extends React.Component<
|
||||
|
||||
api
|
||||
.invoke("GET", `/api/v1/users/${selectedUser.accessKey}`)
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: "",
|
||||
accessKey: res.accessKey,
|
||||
selectedGroups: res.memberOf || [],
|
||||
enabled: res.status
|
||||
enabled: res.status,
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
addError: err,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -184,7 +186,7 @@ class AddUserContent extends React.Component<
|
||||
accessKey,
|
||||
secretKey,
|
||||
selectedGroups,
|
||||
enabled
|
||||
enabled,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
@@ -204,68 +206,67 @@ class AddUserContent extends React.Component<
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<InputBoxWrapper
|
||||
id="accesskey-input"
|
||||
name="accesskey-input"
|
||||
label="Access Key"
|
||||
value={accessKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ accessKey: e.target.value });
|
||||
}}
|
||||
disabled={selectedUser !== null}
|
||||
/>
|
||||
|
||||
{selectedUser !== null ? (
|
||||
<RadioGroupSelector
|
||||
currentSelection={enabled}
|
||||
id="user-status"
|
||||
name="user-status"
|
||||
label="Status"
|
||||
onChange={e => {
|
||||
this.setState({ enabled: e.target.value });
|
||||
}}
|
||||
selectorOptions={[
|
||||
{ label: "Enabled", value: "enabled" },
|
||||
{ label: "Disabled", value: "disabled" }
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<InputBoxWrapper
|
||||
id="standard-multiline-static"
|
||||
name="standard-multiline-static"
|
||||
label="Secret Key"
|
||||
type="password"
|
||||
value={secretKey}
|
||||
id="accesskey-input"
|
||||
name="accesskey-input"
|
||||
label="Access Key"
|
||||
value={accessKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ secretKey: e.target.value });
|
||||
this.setState({ accessKey: e.target.value });
|
||||
}}
|
||||
autoComplete="current-password"
|
||||
disabled={selectedUser !== null}
|
||||
/>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<GroupsSelectors
|
||||
selectedGroups={selectedGroups}
|
||||
setSelectedGroups={(elements: string[]) => {
|
||||
this.setState({
|
||||
selectedGroups: elements
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
|
||||
{selectedUser !== null ? (
|
||||
<RadioGroupSelector
|
||||
currentSelection={enabled}
|
||||
id="user-status"
|
||||
name="user-status"
|
||||
label="Status"
|
||||
onChange={(e) => {
|
||||
this.setState({ enabled: e.target.value });
|
||||
}}
|
||||
selectorOptions={[
|
||||
{ label: "Enabled", value: "enabled" },
|
||||
{ label: "Disabled", value: "disabled" },
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<InputBoxWrapper
|
||||
id="standard-multiline-static"
|
||||
name="standard-multiline-static"
|
||||
label="Secret Key"
|
||||
type="password"
|
||||
value={secretKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ secretKey: e.target.value });
|
||||
}}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<GroupsSelectors
|
||||
selectedGroups={selectedGroups}
|
||||
setSelectedGroups={(elements: string[]) => {
|
||||
this.setState({
|
||||
selectedGroups: elements,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
|
||||
@@ -24,7 +24,7 @@ import InputAdornment from "@material-ui/core/InputAdornment";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import api from "../../../common/api";
|
||||
import { groupsSort } from "../../../utils/sortFunctions";
|
||||
import { stringSort } from "../../../utils/sortFunctions";
|
||||
import { GroupsList } from "../Groups/types";
|
||||
import get from "lodash/get";
|
||||
import TableWrapper from "../Common/TableWrapper/TableWrapper";
|
||||
@@ -38,43 +38,43 @@ interface IGroupsProps {
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
seeMore: {
|
||||
marginTop: theme.spacing(3)
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
paper: {
|
||||
// padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column"
|
||||
flexDirection: "column",
|
||||
},
|
||||
addSideBar: {
|
||||
width: "320px",
|
||||
padding: "20px"
|
||||
padding: "20px",
|
||||
},
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
tableToolbar: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(0)
|
||||
paddingRight: theme.spacing(0),
|
||||
},
|
||||
wrapCell: {
|
||||
maxWidth: "200px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word"
|
||||
wordWrap: "break-word",
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
"& tr": {
|
||||
"& th": {
|
||||
fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
actionsTray: {
|
||||
textAlign: "left",
|
||||
"& button": {
|
||||
marginLeft: 10
|
||||
}
|
||||
marginLeft: 10,
|
||||
},
|
||||
},
|
||||
filterField: {
|
||||
background: "#FFFFFF",
|
||||
@@ -82,24 +82,24 @@ const styles = (theme: Theme) =>
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 3px 6px #00000012",
|
||||
width: "100%",
|
||||
zIndex: 500
|
||||
zIndex: 500,
|
||||
},
|
||||
noFound: {
|
||||
textAlign: "center",
|
||||
padding: "10px 0"
|
||||
padding: "10px 0",
|
||||
},
|
||||
tableContainer: {
|
||||
maxHeight: 200
|
||||
maxHeight: 200,
|
||||
},
|
||||
stickyHeader: {
|
||||
backgroundColor: "#fff"
|
||||
}
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
});
|
||||
|
||||
const GroupsSelectors = ({
|
||||
classes,
|
||||
selectedGroups,
|
||||
setSelectedGroups
|
||||
setSelectedGroups,
|
||||
}: IGroupsProps) => {
|
||||
// Local State
|
||||
const [records, setRecords] = useState<any[]>([]);
|
||||
@@ -129,11 +129,11 @@ const GroupsSelectors = ({
|
||||
if (!groups) {
|
||||
groups = [];
|
||||
}
|
||||
setRecords(groups.sort(groupsSort));
|
||||
setRecords(groups.sort(stringSort));
|
||||
setError("");
|
||||
isLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setError(err);
|
||||
isLoading(false);
|
||||
});
|
||||
@@ -151,14 +151,14 @@ const GroupsSelectors = ({
|
||||
elements.push(value);
|
||||
} else {
|
||||
// User has unchecked this field, we need to remove it from the list
|
||||
elements = elements.filter(element => element !== value);
|
||||
elements = elements.filter((element) => element !== value);
|
||||
}
|
||||
setSelectedGroups(elements);
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
const filteredRecords = records.filter(elementItem =>
|
||||
const filteredRecords = records.filter((elementItem) =>
|
||||
elementItem.includes(filter)
|
||||
);
|
||||
|
||||
@@ -183,9 +183,9 @@ const GroupsSelectors = ({
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
)
|
||||
),
|
||||
}}
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
setFilter(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
Grid,
|
||||
Typography,
|
||||
TextField,
|
||||
InputAdornment
|
||||
InputAdornment,
|
||||
} from "@material-ui/core";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import GroupIcon from "@material-ui/icons/Group";
|
||||
@@ -38,50 +38,50 @@ import TableWrapper from "../Common/TableWrapper/TableWrapper";
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
seeMore: {
|
||||
marginTop: theme.spacing(3)
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
paper: {
|
||||
// padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column"
|
||||
flexDirection: "column",
|
||||
},
|
||||
addSideBar: {
|
||||
width: "320px",
|
||||
padding: "20px"
|
||||
padding: "20px",
|
||||
},
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
color: "red",
|
||||
},
|
||||
tableToolbar: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(0)
|
||||
paddingRight: theme.spacing(0),
|
||||
},
|
||||
wrapCell: {
|
||||
maxWidth: "200px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word"
|
||||
wordWrap: "break-word",
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
"& tr": {
|
||||
"& th": {
|
||||
fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
actionsTray: {
|
||||
textAlign: "right",
|
||||
"& button": {
|
||||
marginLeft: 10
|
||||
}
|
||||
marginLeft: 10,
|
||||
},
|
||||
},
|
||||
searchField: {
|
||||
background: "#FFFFFF",
|
||||
padding: 12,
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 3px 6px #00000012"
|
||||
}
|
||||
boxShadow: "0px 3px 6px #00000012",
|
||||
},
|
||||
});
|
||||
|
||||
interface IUsersProps {
|
||||
@@ -118,7 +118,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
selectedUser: null,
|
||||
addGroupOpen: false,
|
||||
filter: "",
|
||||
checkedUsers: []
|
||||
checkedUsers: [],
|
||||
};
|
||||
|
||||
fetchRecords() {
|
||||
@@ -133,7 +133,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
loading: false,
|
||||
records: users.sort(usersSort),
|
||||
totalRecords: users.length,
|
||||
error: ""
|
||||
error: "",
|
||||
});
|
||||
// if we get 0 results, and page > 0 , go down 1 page
|
||||
if ((!users || users.length === 0) && page > 0) {
|
||||
@@ -143,7 +143,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.setState({ loading: false, error: err });
|
||||
});
|
||||
});
|
||||
@@ -191,28 +191,29 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
selectedUser,
|
||||
filter,
|
||||
checkedUsers,
|
||||
addGroupOpen
|
||||
addGroupOpen,
|
||||
} = this.state;
|
||||
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
this.setState({ page: newPage }, () => {
|
||||
this.fetchRecords();
|
||||
});
|
||||
this.setState({ page: newPage });
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const rPP = parseInt(event.target.value, 10);
|
||||
this.setState({ page: 0, rowsPerPage: rPP }, () => {
|
||||
this.fetchRecords();
|
||||
});
|
||||
this.setState({ page: 0, rowsPerPage: rPP });
|
||||
};
|
||||
|
||||
const filteredRecords = records.filter(elementItem =>
|
||||
const filteredRecords = records.filter((elementItem) =>
|
||||
elementItem.accessKey.includes(filter)
|
||||
);
|
||||
|
||||
const beginRecord = page * rowsPerPage;
|
||||
const endRecords = beginRecord + rowsPerPage;
|
||||
|
||||
const paginatedRecords = filteredRecords.slice(beginRecord, endRecords);
|
||||
|
||||
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const targetD = e.target;
|
||||
const value = targetD.value;
|
||||
@@ -225,11 +226,11 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
elements.push(value);
|
||||
} else {
|
||||
// User has unchecked this field, we need to remove it from the list
|
||||
elements = elements.filter(element => element !== value);
|
||||
elements = elements.filter((element) => element !== value);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
checkedUsers: elements
|
||||
checkedUsers: elements,
|
||||
});
|
||||
|
||||
return elements;
|
||||
@@ -238,20 +239,20 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
const viewAction = (selectionElement: any): void => {
|
||||
this.setState({
|
||||
addScreenOpen: true,
|
||||
selectedUser: selectionElement
|
||||
selectedUser: selectionElement,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAction = (selectionElement: any): void => {
|
||||
this.setState({
|
||||
deleteOpen: true,
|
||||
selectedUser: selectionElement
|
||||
selectedUser: selectionElement,
|
||||
});
|
||||
};
|
||||
|
||||
const tableActions = [
|
||||
{ type: "view", onClick: viewAction },
|
||||
{ type: "delete", onClick: deleteAction }
|
||||
{ type: "delete", onClick: deleteAction },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -303,10 +304,10 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
)
|
||||
),
|
||||
}}
|
||||
onChange={e => {
|
||||
this.setState({ filter: e.target.value });
|
||||
onChange={(e) => {
|
||||
this.setState({ filter: e.target.value, page: 0 });
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
@@ -317,7 +318,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
onClick={() => {
|
||||
if (checkedUsers.length > 0) {
|
||||
this.setState({
|
||||
addGroupOpen: true
|
||||
addGroupOpen: true,
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -331,7 +332,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
addScreenOpen: true,
|
||||
selectedUser: null
|
||||
selectedUser: null,
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -349,22 +350,22 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
onSelect={selectionChanged}
|
||||
selectedItems={checkedUsers}
|
||||
isLoading={loading}
|
||||
records={filteredRecords}
|
||||
records={paginatedRecords}
|
||||
entityName="Users"
|
||||
idField="accessKey"
|
||||
paginatorConfig={{
|
||||
rowsPerPageOptions: [5, 10, 25],
|
||||
colSpan: 3,
|
||||
count: totalRecords,
|
||||
count: filteredRecords.length,
|
||||
rowsPerPage: rowsPerPage,
|
||||
page: page,
|
||||
SelectProps: {
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true
|
||||
native: true,
|
||||
},
|
||||
onChangePage: handleChangePage,
|
||||
onChangeRowsPerPage: handleChangeRowsPerPage,
|
||||
ActionsComponent: MinTablePaginationActions
|
||||
ActionsComponent: MinTablePaginationActions,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
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));
|
||||
46
portal-ui/src/screens/Console/Watch/actions.ts
Normal file
46
portal-ui/src/screens/Console/Watch/actions.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
}
|
||||
32
portal-ui/src/screens/Console/actions.ts
Normal file
32
portal-ui/src/screens/Console/actions.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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 { ISessionResponse } from "./types";
|
||||
|
||||
export const SESSION_RESPONSE = "SESSION_RESPONSE";
|
||||
|
||||
interface SessionAction {
|
||||
type: typeof SESSION_RESPONSE;
|
||||
message: ISessionResponse;
|
||||
}
|
||||
export type SessionActionTypes = SessionAction;
|
||||
|
||||
export function saveSessionResponse(message: ISessionResponse) {
|
||||
return {
|
||||
type: SESSION_RESPONSE,
|
||||
message: message,
|
||||
};
|
||||
}
|
||||
44
portal-ui/src/screens/Console/reducer.ts
Normal file
44
portal-ui/src/screens/Console/reducer.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { ISessionResponse } from "./types";
|
||||
import { SessionActionTypes, SESSION_RESPONSE } from "./actions";
|
||||
|
||||
export interface ConsoleState {
|
||||
session: ISessionResponse;
|
||||
}
|
||||
|
||||
const initialState: ConsoleState = {
|
||||
session: {
|
||||
status: "",
|
||||
pages: [],
|
||||
},
|
||||
};
|
||||
|
||||
export function consoleReducer(
|
||||
state = initialState,
|
||||
action: SessionActionTypes
|
||||
): ConsoleState {
|
||||
switch (action.type) {
|
||||
case SESSION_RESPONSE:
|
||||
return {
|
||||
...state,
|
||||
session: action.message,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
20
portal-ui/src/screens/Console/types.ts
Normal file
20
portal-ui/src/screens/Console/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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 ISessionResponse {
|
||||
status: string;
|
||||
pages: string[];
|
||||
}
|
||||
@@ -14,23 +14,23 @@
|
||||
// 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, {FC, useEffect} from "react";
|
||||
import {RouteComponentProps} from "react-router";
|
||||
import React, { FC, useEffect } from "react";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import storage from "local-storage-fallback";
|
||||
import api from "../../common/api";
|
||||
|
||||
const LoginCallback: FC<RouteComponentProps> = ({location}) => {
|
||||
const LoginCallback: FC<RouteComponentProps> = ({ location }) => {
|
||||
useEffect(() => {
|
||||
const code = (location.search.match(/code=([^&]+)/) || [])[1];
|
||||
const state = (location.search.match(/state=([^&]+)/) || [])[1];
|
||||
api
|
||||
.invoke("POST", "/api/v1/login/oauth2/auth", {code, state})
|
||||
.invoke("POST", "/api/v1/login/oauth2/auth", { code, state })
|
||||
.then((res: any) => {
|
||||
if (res && res.sessionId) {
|
||||
// store the jwt token
|
||||
storage.setItem("token", res.sessionId);
|
||||
// We push to history the new URL.
|
||||
window.location.href = "/dashboard";
|
||||
window.location.href = "/";
|
||||
}
|
||||
})
|
||||
.catch((res: any) => {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { userLoggedIn } from "../../actions";
|
||||
import history from "../../history";
|
||||
import api from "../../common/api";
|
||||
import { ILoginDetails } from "./types";
|
||||
import { setCookie } from "../../common/utils";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -153,6 +154,7 @@ class Login extends React.Component<ILoginProps, ILoginState> {
|
||||
|
||||
if (bodyResponse.sessionId) {
|
||||
// store the jwt token
|
||||
setCookie("token", bodyResponse.sessionId);
|
||||
storage.setItem("token", bodyResponse.sessionId);
|
||||
//return res.body.sessionId;
|
||||
} else if (bodyResponse.error) {
|
||||
@@ -163,8 +165,9 @@ class Login extends React.Component<ILoginProps, ILoginState> {
|
||||
.then(() => {
|
||||
// We set the state in redux
|
||||
this.props.userLoggedIn(true);
|
||||
// We push to history the new URL.
|
||||
history.push("/dashboard");
|
||||
// There is a browser cache issue if we change the policy associated to an account and then logout and history.push("/") after login
|
||||
// therefore after login we need to use window.location redirect
|
||||
window.location.href = "/";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setState({ error: `${err}` });
|
||||
|
||||
@@ -23,22 +23,21 @@ import Container from "@material-ui/core/Container";
|
||||
import Copyright from "../common/Copyright";
|
||||
import history from "../history";
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
"@global": {
|
||||
body: {
|
||||
backgroundColor: theme.palette.common.white
|
||||
}
|
||||
backgroundColor: theme.palette.common.white,
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
marginTop: theme.spacing(8),
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center"
|
||||
}
|
||||
alignItems: "center",
|
||||
},
|
||||
}));
|
||||
const NotFound: React.FC = () => {
|
||||
const classes = useStyles();
|
||||
console.log(history);
|
||||
return (
|
||||
<Container component="main">
|
||||
<CssBaseline />
|
||||
|
||||
@@ -18,10 +18,16 @@ import { applyMiddleware, combineReducers, compose, createStore } from "redux";
|
||||
import thunk from "redux-thunk";
|
||||
import { systemReducer } from "./reducer";
|
||||
import { traceReducer } from "./screens/Console/Trace/reducers";
|
||||
import { logReducer } from "./screens/Console/Logs/reducers";
|
||||
import { watchReducer } from "./screens/Console/Watch/reducers";
|
||||
import { consoleReducer } from "./screens/Console/reducer";
|
||||
|
||||
const globalReducer = combineReducers({
|
||||
system: systemReducer,
|
||||
trace: traceReducer
|
||||
trace: traceReducer,
|
||||
logs: logReducer,
|
||||
watch: watchReducer,
|
||||
console: consoleReducer,
|
||||
});
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -29,7 +29,7 @@ export const usersSort = (a: userInterface, b: userInterface) => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const groupsSort = (a: string, b: string) => {
|
||||
export const stringSort = (a: string, b: string) => {
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
22
portal-ui/src/utils/wsUtils.ts
Normal file
22
portal-ui/src/utils/wsUtils.ts
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/>.
|
||||
export const wsProtocol = (protocol: string): string => {
|
||||
let wsProtocol = "ws";
|
||||
if (protocol === "https:") {
|
||||
wsProtocol = "wss";
|
||||
}
|
||||
return wsProtocol;
|
||||
};
|
||||
@@ -17,9 +17,10 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
"jsx": "react",
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2027,6 +2027,11 @@ alphanum-sort@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
|
||||
integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
|
||||
|
||||
anser@^1.4.1:
|
||||
version "1.4.9"
|
||||
resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.9.tgz#1f85423a5dcf8da4631a341665ff675b96845760"
|
||||
integrity sha512-AI+BjTeGt2+WFk4eWcqbQ7snZpDBt8SaLlj0RT2h5xfdWaiy51OjYvqwMrNzJLGy8iOAL6nKDITWO+rd4MkYEA==
|
||||
|
||||
ansi-align@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f"
|
||||
@@ -2096,6 +2101,14 @@ ansi-styles@^4.1.0:
|
||||
"@types/color-name" "^1.1.1"
|
||||
color-convert "^2.0.1"
|
||||
|
||||
ansi-to-react@^6.0.5:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/ansi-to-react/-/ansi-to-react-6.0.5.tgz#b41d4e91dbbf847a373368cfa517f337399aef9b"
|
||||
integrity sha512-kVKsflDGTJP6CnKOrgt7YQGnCurcf4q0rapWTNmx5KcIvgz+wHqjUIkfNmLQcJM6ZeHD0epbSOz0QOLg7fbVmg==
|
||||
dependencies:
|
||||
anser "^1.4.1"
|
||||
escape-carriage "^1.3.0"
|
||||
|
||||
ansicolors@~0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979"
|
||||
@@ -4496,6 +4509,11 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.3:
|
||||
d "^1.0.1"
|
||||
ext "^1.1.2"
|
||||
|
||||
escape-carriage@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/escape-carriage/-/escape-carriage-1.3.0.tgz#71006b2d4da8cb6828686addafcb094239c742f3"
|
||||
integrity sha512-ATWi5MD8QlAGQOeMgI8zTp671BG8aKvAC0M7yenlxU4CRLGO/sKthxVUyjiOFKjHdIo+6dZZUNFgHFeVEaKfGQ==
|
||||
|
||||
escape-html@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
|
||||
@@ -28,12 +28,9 @@ import (
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/minio/minio/pkg/event/target"
|
||||
|
||||
"github.com/minio/minio/cmd/config"
|
||||
|
||||
"github.com/minio/mcs/models"
|
||||
|
||||
"github.com/minio/minio/cmd/config"
|
||||
"github.com/minio/minio/pkg/event/target"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
)
|
||||
|
||||
@@ -532,7 +529,7 @@ func Test_getConfig(t *testing.T) {
|
||||
|
||||
// mock function response from getConfig()
|
||||
minioGetConfigKVMock = func(key string) ([]byte, error) {
|
||||
return nil, errors.New("Invalid config")
|
||||
return nil, errors.New("invalid config")
|
||||
}
|
||||
|
||||
mockConfigList := madmin.Help{}
|
||||
@@ -553,7 +550,7 @@ func Test_getConfig(t *testing.T) {
|
||||
mock: func() {
|
||||
// mock function response from getConfig()
|
||||
minioGetConfigKVMock = func(key string) ([]byte, error) {
|
||||
return nil, errors.New("Invalid config")
|
||||
return nil, errors.New("invalid config")
|
||||
}
|
||||
// mock function response from listConfig()
|
||||
minioHelpConfigKVMock = func(subSys, key string, envOnly bool) (madmin.Help, error) {
|
||||
|
||||
95
restapi/admin_console.go
Normal file
95
restapi/admin_console.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// 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"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
)
|
||||
|
||||
const logTimeFormat string = "15:04:05 MST 01/02/2006"
|
||||
|
||||
// startConsoleLog starts log of the servers
|
||||
func startConsoleLog(ctx context.Context, conn WSConn, client MinioAdmin) error {
|
||||
// TODO: accept parameters as variables
|
||||
// name of node, default = "" (all)
|
||||
node := ""
|
||||
// number of log lines
|
||||
lineCount := 100
|
||||
// type of logs "minio"|"application"|"all" default = "all"
|
||||
logKind := "all"
|
||||
// Start listening on all Console Log activity.
|
||||
logCh := client.getLogs(ctx, node, lineCount, logKind)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case logInfo, ok := <-logCh:
|
||||
// zero value returned because the channel is closed and empty
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if logInfo.Err != nil {
|
||||
log.Println("error on console logs:", logInfo.Err)
|
||||
return logInfo.Err
|
||||
}
|
||||
|
||||
// Serialize message to be sent
|
||||
bytes, err := json.Marshal(serializeConsoleLogInfo(&logInfo))
|
||||
if err != nil {
|
||||
fmt.Println("error on json.Marshal:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Send Message through websocket connection
|
||||
err = conn.writeMessage(websocket.TextMessage, bytes)
|
||||
if err != nil {
|
||||
log.Println("error writeMessage:", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func serializeConsoleLogInfo(l *madmin.LogInfo) (logInfo madmin.LogInfo) {
|
||||
logInfo = *l
|
||||
if logInfo.ConsoleMsg != "" {
|
||||
if strings.HasPrefix(logInfo.ConsoleMsg, "\n") {
|
||||
logInfo.ConsoleMsg = strings.TrimPrefix(logInfo.ConsoleMsg, "\n")
|
||||
}
|
||||
}
|
||||
if logInfo.Time != "" {
|
||||
logInfo.Time = getLogTime(logInfo.Time)
|
||||
}
|
||||
return logInfo
|
||||
}
|
||||
|
||||
func getLogTime(lt string) string {
|
||||
tm, err := time.Parse(time.RFC3339Nano, lt)
|
||||
if err != nil {
|
||||
return lt
|
||||
}
|
||||
return tm.Format(logTimeFormat)
|
||||
}
|
||||
125
restapi/admin_console_test.go
Normal file
125
restapi/admin_console_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// 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"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// assigning mock at runtime instead of compile time
|
||||
var minioGetLogsMock func(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo
|
||||
|
||||
// mock function of listPolicies()
|
||||
func (ac adminClientMock) getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
|
||||
return minioGetLogsMock(ctx, node, lineCnt, logKind)
|
||||
}
|
||||
|
||||
func TestAdminConsoleLog(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
mockWSConn := mockConn{}
|
||||
function := "startConsoleLog(ctx, )"
|
||||
ctx := context.Background()
|
||||
|
||||
testReceiver := make(chan madmin.LogInfo, 5)
|
||||
textToReceive := "test message"
|
||||
testStreamSize := 5
|
||||
isClosed := false // testReceiver is closed?
|
||||
|
||||
// Test-1: Serve Console with no errors until Console finishes sending
|
||||
// define mock function behavior for minio server Console
|
||||
minioGetLogsMock = func(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
|
||||
ch := make(chan madmin.LogInfo)
|
||||
// Only success, start a routine to start reading line by line.
|
||||
go func(ch chan<- madmin.LogInfo) {
|
||||
defer close(ch)
|
||||
lines := make([]int, testStreamSize)
|
||||
// mocking sending 5 lines of info
|
||||
for range lines {
|
||||
info := madmin.LogInfo{
|
||||
ConsoleMsg: textToReceive,
|
||||
}
|
||||
ch <- info
|
||||
}
|
||||
}(ch)
|
||||
return ch
|
||||
}
|
||||
writesCount := 1
|
||||
// mock connection WriteMessage() no error
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
// emulate that receiver gets the message written
|
||||
var t madmin.LogInfo
|
||||
_ = json.Unmarshal(data, &t)
|
||||
if writesCount == testStreamSize {
|
||||
if !isClosed {
|
||||
close(testReceiver)
|
||||
isClosed = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
testReceiver <- t
|
||||
writesCount++
|
||||
return nil
|
||||
}
|
||||
if err := startConsoleLog(ctx, mockWSConn, adminClient); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// check that the TestReceiver got the same number of data from Console.
|
||||
for i := range testReceiver {
|
||||
assert.Equal(textToReceive, i.ConsoleMsg)
|
||||
}
|
||||
|
||||
// Test-2: if error happens while writing, return error
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
return fmt.Errorf("error on write")
|
||||
}
|
||||
if err := startConsoleLog(ctx, mockWSConn, adminClient); assert.Error(err) {
|
||||
assert.Equal("error on write", err.Error())
|
||||
}
|
||||
|
||||
// Test-3: error happens on GetLogs Minio, Console should stop
|
||||
// and error shall be returned.
|
||||
minioGetLogsMock = func(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
|
||||
ch := make(chan madmin.LogInfo)
|
||||
// Only success, start a routine to start reading line by line.
|
||||
go func(ch chan<- madmin.LogInfo) {
|
||||
defer close(ch)
|
||||
lines := make([]int, 2)
|
||||
// mocking sending 5 lines of info
|
||||
for range lines {
|
||||
info := madmin.LogInfo{
|
||||
ConsoleMsg: textToReceive,
|
||||
}
|
||||
ch <- info
|
||||
}
|
||||
ch <- madmin.LogInfo{Err: fmt.Errorf("error on Console")}
|
||||
}(ch)
|
||||
return ch
|
||||
}
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
return nil
|
||||
}
|
||||
if err := startConsoleLog(ctx, mockWSConn, adminClient); assert.Error(err) {
|
||||
assert.Equal("error on Console", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -41,21 +41,21 @@ func registerAdminInfoHandlers(api *operations.McsAPI) {
|
||||
|
||||
}
|
||||
|
||||
type UsageInfo struct {
|
||||
type usageInfo struct {
|
||||
Buckets int64
|
||||
Objects int64
|
||||
Usage int64
|
||||
}
|
||||
|
||||
// getAdminInfo invokes admin info and returns a parsed `UsageInfo` structure
|
||||
func getAdminInfo(ctx context.Context, client MinioAdmin) (*UsageInfo, error) {
|
||||
// getAdminInfo invokes admin info and returns a parsed `usageInfo` structure
|
||||
func getAdminInfo(ctx context.Context, client MinioAdmin) (*usageInfo, error) {
|
||||
serverInfo, err := client.serverInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// we are trimming uint64 to int64 this will report an incorrect measurement for numbers greater than
|
||||
// 9,223,372,036,854,775,807
|
||||
return &UsageInfo{
|
||||
return &usageInfo{
|
||||
Buckets: int64(serverInfo.Buckets.Count),
|
||||
Objects: int64(serverInfo.Objects.Count),
|
||||
Usage: int64(serverInfo.Usage.Size),
|
||||
|
||||
@@ -21,10 +21,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
@@ -50,125 +48,41 @@ type callStats struct {
|
||||
Ttfb string `json:"timeToFirstByte"`
|
||||
}
|
||||
|
||||
// trace serves madmin.ServiceTraceInfo
|
||||
// on a Websocket connection.
|
||||
func (wsc *wsClient) trace() {
|
||||
defer func() {
|
||||
log.Println("trace stopped")
|
||||
// close connection after return
|
||||
wsc.conn.close()
|
||||
}()
|
||||
log.Println("trace started")
|
||||
|
||||
err := startTraceInfo(wsc.conn, wsc.madmin)
|
||||
// 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, ""))
|
||||
}
|
||||
|
||||
// startTraceInfo starts trace of the servers
|
||||
// by first 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, sendTraceInfo()
|
||||
// or wsReadCheck() returns, trace should end.
|
||||
func startTraceInfo(conn WSConn, client MinioAdmin) (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()
|
||||
// waitsuntil counter is zero (all are done)
|
||||
wg.Add(3)
|
||||
// start go routine for reading websocket heartbeat
|
||||
readErr := wsReadCheck(ctx, &wg, conn)
|
||||
// send Stream of Trace Info to the ws c.connection
|
||||
traceCh := sendTraceInfo(ctx, &wg, conn, client)
|
||||
// If wsReadCheck returns it means that it is not possible to check
|
||||
// ws heartbeat anymore so we stop from doing trace, 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)
|
||||
|
||||
// wait for traceCh to finish
|
||||
if err := <-traceCh; err != nil {
|
||||
mError = err
|
||||
}
|
||||
|
||||
// if traceCh closes for any reason,
|
||||
// cancel context for all goroutines
|
||||
cancel()
|
||||
// wait all goroutines to finish
|
||||
wg.Wait()
|
||||
return mError
|
||||
}
|
||||
|
||||
// sendTraceInfo sends stream of Trace Info to the ws connection
|
||||
func sendTraceInfo(ctx context.Context, wg *sync.WaitGroup, conn WSConn, client MinioAdmin) <-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)
|
||||
|
||||
// trace all traffic
|
||||
allTraffic := true
|
||||
// Trace failed requests only
|
||||
errOnly := false
|
||||
// Start listening on all trace activity.
|
||||
traceCh := client.serviceTrace(ctx, allTraffic, errOnly)
|
||||
|
||||
for traceInfo := range traceCh {
|
||||
func startTraceInfo(ctx context.Context, conn WSConn, client MinioAdmin) error {
|
||||
// trace all traffic
|
||||
allTraffic := true
|
||||
// Trace failed requests only
|
||||
errOnly := false
|
||||
// Start listening on all trace activity.
|
||||
traceCh := client.serviceTrace(ctx, allTraffic, errOnly)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case traceInfo, ok := <-traceCh:
|
||||
// zero value returned because the channel is closed and empty
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if traceInfo.Err != nil {
|
||||
log.Println("error on serviceTrace:", traceInfo.Err)
|
||||
ch <- traceInfo.Err
|
||||
return
|
||||
return traceInfo.Err
|
||||
}
|
||||
// Serialize message to be sent
|
||||
traceInfoBytes, err := json.Marshal(shortTrace(&traceInfo))
|
||||
if err != nil {
|
||||
fmt.Println("error on json.Marshal:", err)
|
||||
ch <- err
|
||||
return
|
||||
return err
|
||||
}
|
||||
// Send Message through websocket connection
|
||||
err = conn.writeMessage(websocket.TextMessage, traceInfoBytes)
|
||||
if err != nil {
|
||||
log.Println("error writeMessage:", err)
|
||||
ch <- err
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
// TODO: verbose
|
||||
}(ch)
|
||||
|
||||
return ch
|
||||
}
|
||||
}
|
||||
|
||||
// shortTrace creates a shorter Trace Info message.
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
trace "github.com/minio/minio/pkg/trace"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -40,12 +39,13 @@ func TestAdminTrace(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
mockWSConn := mockConn{}
|
||||
wsClientMock := wsClientMock{madmin: adminClient}
|
||||
function := "startTraceInfo()"
|
||||
function := "startTraceInfo(ctx, )"
|
||||
ctx := context.Background()
|
||||
|
||||
testReceiver := make(chan shortTraceMsg, 5)
|
||||
textToReceive := "test"
|
||||
testStreamSize := 5
|
||||
isClosed := false // testReceiver is closed?
|
||||
|
||||
// Test-1: Serve Trace with no errors until trace finishes sending
|
||||
// define mock function behavior for minio server Trace
|
||||
@@ -65,10 +65,6 @@ func TestAdminTrace(t *testing.T) {
|
||||
}(ch)
|
||||
return ch
|
||||
}
|
||||
// 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 {
|
||||
@@ -77,14 +73,17 @@ func TestAdminTrace(t *testing.T) {
|
||||
_ = json.Unmarshal(data, &t)
|
||||
if writesCount == testStreamSize {
|
||||
// for testing we need to close the receiver channel
|
||||
close(testReceiver)
|
||||
if !isClosed {
|
||||
close(testReceiver)
|
||||
isClosed = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
testReceiver <- t
|
||||
writesCount++
|
||||
return nil
|
||||
}
|
||||
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); err != nil {
|
||||
if err := startTraceInfo(ctx, mockWSConn, adminClient); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// check that the TestReceiver got the same number of data from trace.
|
||||
@@ -96,50 +95,11 @@ func TestAdminTrace(t *testing.T) {
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
return fmt.Errorf("error on write")
|
||||
}
|
||||
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) {
|
||||
if err := startTraceInfo(ctx, mockWSConn, adminClient); 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 := startTraceInfo(mockWSConn, wsClientMock.madmin); 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 trace.
|
||||
connReadMessageMock = func() (messageType int, p []byte, err error) {
|
||||
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""}
|
||||
}
|
||||
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); err != nil {
|
||||
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 := startTraceInfo(mockWSConn, wsClientMock.madmin); 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 := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) {
|
||||
assert.Equal("error on read", err.Error())
|
||||
}
|
||||
|
||||
// Test-7: error happens on serviceTrace Minio, trace should stop
|
||||
// Test-3: error happens on serviceTrace Minio, trace should stop
|
||||
// and error shall be returned.
|
||||
minioServiceTraceMock = func(ctx context.Context, allTrace, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
ch := make(chan madmin.ServiceTraceInfo)
|
||||
@@ -158,12 +118,10 @@ func TestAdminTrace(t *testing.T) {
|
||||
}(ch)
|
||||
return ch
|
||||
}
|
||||
// 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
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
return nil
|
||||
}
|
||||
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) {
|
||||
if err := startTraceInfo(ctx, mockWSConn, adminClient); assert.Error(err) {
|
||||
assert.Equal("error on trace", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ func NewAdminClient(url, accessKey, secretKey string) (*madmin.AdminClient, *pro
|
||||
AppName: appName,
|
||||
AppVersion: McsVersion,
|
||||
AppComments: []string{appName, runtime.GOOS, runtime.GOARCH},
|
||||
Insecure: false,
|
||||
})
|
||||
s3Client.SetCustomTransport(STSClient.Transport)
|
||||
if err != nil {
|
||||
return nil, err.Trace(url)
|
||||
}
|
||||
@@ -81,8 +83,12 @@ type MinioAdmin interface {
|
||||
startProfiling(ctx context.Context, profiler madmin.ProfilerType) ([]madmin.StartProfilingResult, error)
|
||||
stopProfiling(ctx context.Context) (io.ReadCloser, error)
|
||||
serviceTrace(ctx context.Context, allTrace, errTrace bool) <-chan madmin.ServiceTraceInfo
|
||||
getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo
|
||||
accountUsageInfo(ctx context.Context) (madmin.AccountUsageInfo, error)
|
||||
// Service Accounts
|
||||
addServiceAccount(ctx context.Context, policy *iampolicy.Policy) (mauth.Credentials, error)
|
||||
listServiceAccounts(ctx context.Context) (madmin.ListServiceAccountsResp, error)
|
||||
deleteServiceAccount(ctx context.Context, serviceAccount string) error
|
||||
}
|
||||
|
||||
// Interface implementation
|
||||
@@ -203,23 +209,54 @@ func (ac adminClient) serviceTrace(ctx context.Context, allTrace, errTrace bool)
|
||||
return ac.client.ServiceTrace(ctx, allTrace, errTrace)
|
||||
}
|
||||
|
||||
// implements madmin.GetLogs()
|
||||
func (ac adminClient) getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
|
||||
return ac.client.GetLogs(ctx, node, lineCnt, logKind)
|
||||
}
|
||||
|
||||
// implements madmin.AddServiceAccount()
|
||||
func (ac adminClient) addServiceAccount(ctx context.Context, policy *iampolicy.Policy) (mauth.Credentials, error) {
|
||||
return ac.client.AddServiceAccount(ctx, policy)
|
||||
}
|
||||
|
||||
// implements madmin.ListServiceAccounts()
|
||||
func (ac adminClient) listServiceAccounts(ctx context.Context) (madmin.ListServiceAccountsResp, error) {
|
||||
return ac.client.ListServiceAccounts(ctx)
|
||||
}
|
||||
|
||||
// implements madmin.DeleteServiceAccount()
|
||||
func (ac adminClient) deleteServiceAccount(ctx context.Context, serviceAccount string) error {
|
||||
return ac.client.DeleteServiceAccount(ctx, serviceAccount)
|
||||
}
|
||||
|
||||
// implements madmin.AccountingUsageInfo()
|
||||
func (ac adminClient) accountUsageInfo(ctx context.Context) (madmin.AccountUsageInfo, error) {
|
||||
return ac.client.AccountUsageInfo(ctx)
|
||||
}
|
||||
|
||||
func newMAdminClient(jwt string) (*madmin.AdminClient, error) {
|
||||
claims, err := auth.JWTAuthenticate(jwt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminClient, err := newAdminFromClaims(claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return adminClient, nil
|
||||
}
|
||||
|
||||
// newAdminFromClaims creates a minio admin from Decrypted claims using Assume role credentials
|
||||
func newAdminFromClaims(claims *auth.DecryptedClaims) (*madmin.AdminClient, error) {
|
||||
tlsEnabled := getMinIOEndpointIsSecure()
|
||||
adminClient, err := madmin.NewWithOptions(getMinIOEndpoint(), &madmin.Options{
|
||||
Creds: credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken),
|
||||
Secure: getMinIOEndpointIsSecure(),
|
||||
Secure: tlsEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminClient.SetCustomTransport(STSClient.Transport)
|
||||
return adminClient, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,15 @@ package restapi
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"errors"
|
||||
|
||||
mc "github.com/minio/mc/cmd"
|
||||
"github.com/minio/mc/pkg/probe"
|
||||
"github.com/minio/mcs/pkg/auth"
|
||||
xjwt "github.com/minio/mcs/pkg/auth/jwt"
|
||||
"github.com/minio/mcs/pkg/auth/ldap"
|
||||
"github.com/minio/minio-go/v6"
|
||||
"github.com/minio/minio-go/v6/pkg/credentials"
|
||||
)
|
||||
@@ -90,6 +94,7 @@ func (c minioClient) getBucketPolicy(bucketName string) (string, error) {
|
||||
type MCS3Client interface {
|
||||
addNotificationConfig(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error
|
||||
removeNotificationConfig(arn string, event string, prefix string, suffix string) *probe.Error
|
||||
watch(options mc.WatchOptions) (*mc.WatchObject, *probe.Error)
|
||||
}
|
||||
|
||||
// Interface implementation
|
||||
@@ -110,7 +115,11 @@ func (c mcS3Client) removeNotificationConfig(arn string, event string, prefix st
|
||||
return c.client.RemoveNotificationConfig(arn, event, prefix, suffix)
|
||||
}
|
||||
|
||||
// Define MCSCredentials interface with all functions to be implemented
|
||||
func (c mcS3Client) watch(options mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
|
||||
return c.client.Watch(options)
|
||||
}
|
||||
|
||||
// MCSCredentials interface with all functions to be implemented
|
||||
// by mock when testing, it should include all needed minioCredentials.Credentials api calls
|
||||
// that are used within this project.
|
||||
type MCSCredentials interface {
|
||||
@@ -133,19 +142,77 @@ func (c mcsCredentials) Expire() {
|
||||
c.minioCredentials.Expire()
|
||||
}
|
||||
|
||||
// mcsSTSAssumeRole it's a STSAssumeRole wrapper, in general
|
||||
// there's no need to use this struct anywhere else in the project, it's only required
|
||||
// for passing a custom *http.Client to *credentials.STSAssumeRole
|
||||
type mcsSTSAssumeRole struct {
|
||||
stsAssumeRole *credentials.STSAssumeRole
|
||||
}
|
||||
|
||||
func (s mcsSTSAssumeRole) Retrieve() (credentials.Value, error) {
|
||||
return s.stsAssumeRole.Retrieve()
|
||||
}
|
||||
|
||||
func (s mcsSTSAssumeRole) IsExpired() bool {
|
||||
return s.stsAssumeRole.IsExpired()
|
||||
}
|
||||
|
||||
// STSClient contains http.client configuration need it by STSAssumeRole
|
||||
var STSClient = PrepareSTSClient()
|
||||
|
||||
func newMcsCredentials(accessKey, secretKey, location string) (*credentials.Credentials, error) {
|
||||
return credentials.NewSTSAssumeRole(getMinIOServer(), credentials.STSAssumeRoleOptions{
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
Location: location,
|
||||
DurationSeconds: xjwt.GetMcsSTSAndJWTDurationInSeconds(),
|
||||
})
|
||||
mcsEndpoint := getMinIOServer()
|
||||
if mcsEndpoint == "" {
|
||||
return nil, errors.New("endpoint cannot be empty for AssumeRoleSTS")
|
||||
}
|
||||
if accessKey == "" || secretKey == "" {
|
||||
return nil, errors.New("creredentials access/secretkey is mandatory for AssumeRoleSTS")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetClaimsFromJWT decrypt and returns the claims associated to a provided jwt
|
||||
func GetClaimsFromJWT(jwt string) (*auth.DecryptedClaims, error) {
|
||||
claims, err := auth.JWTAuthenticate(jwt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// getMcsCredentialsFromJWT returns the *minioCredentials.Credentials associated to the
|
||||
// provided jwt, this is useful for running the Expire() or IsExpired() operations
|
||||
func getMcsCredentialsFromJWT(jwt string) (*credentials.Credentials, error) {
|
||||
claims, err := auth.JWTAuthenticate(jwt)
|
||||
claims, err := GetClaimsFromJWT(jwt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -160,30 +227,35 @@ func newMinioClient(jwt string) (*minio.Client, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{
|
||||
minioClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{
|
||||
Creds: creds,
|
||||
Secure: getMinIOEndpointIsSecure(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return adminClient, nil
|
||||
minioClient.SetCustomTransport(STSClient.Transport)
|
||||
return minioClient, nil
|
||||
}
|
||||
|
||||
// newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket
|
||||
func newS3BucketClient(bucketName *string) (*mc.S3Client, error) {
|
||||
func newS3BucketClient(jwt string, bucketName string) (*mc.S3Client, error) {
|
||||
endpoint := getMinIOServer()
|
||||
accessKeyID := getAccessKey()
|
||||
secretAccessKey := getSecretKey()
|
||||
useSSL := getMinIOEndpointIsSecure()
|
||||
|
||||
if bucketName != nil {
|
||||
endpoint += fmt.Sprintf("/%s", *bucketName)
|
||||
}
|
||||
s3Config := newS3Config(endpoint, accessKeyID, secretAccessKey, !useSSL)
|
||||
client, err := mc.S3New(s3Config)
|
||||
claims, err := auth.JWTAuthenticate(jwt)
|
||||
if err != nil {
|
||||
return nil, err.Cause
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.TrimSpace(bucketName) != "" {
|
||||
endpoint += fmt.Sprintf("/%s", bucketName)
|
||||
}
|
||||
|
||||
s3Config := newS3Config(endpoint, claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken, !useSSL)
|
||||
client, pErr := mc.S3New(s3Config)
|
||||
if pErr != nil {
|
||||
return nil, pErr.Cause
|
||||
}
|
||||
s3Client, ok := client.(*mc.S3Client)
|
||||
if !ok {
|
||||
@@ -195,7 +267,7 @@ func newS3BucketClient(bucketName *string) (*mc.S3Client, error) {
|
||||
|
||||
// newS3Config simply creates a new Config struct using the passed
|
||||
// parameters.
|
||||
func newS3Config(endpoint, accessKey, secretKey string, isSecure bool) *mc.Config {
|
||||
func newS3Config(endpoint, accessKey, secretKey, sessionToken string, isSecure bool) *mc.Config {
|
||||
// We have a valid alias and hostConfig. We populate the
|
||||
// minioCredentials from the match found in the config file.
|
||||
s3Config := new(mc.Config)
|
||||
@@ -209,6 +281,7 @@ func newS3Config(endpoint, accessKey, secretKey string, isSecure bool) *mc.Confi
|
||||
s3Config.HostURL = endpoint
|
||||
s3Config.AccessKey = accessKey
|
||||
s3Config.SecretKey = secretKey
|
||||
s3Config.SessionToken = sessionToken
|
||||
s3Config.Signature = "S3v4"
|
||||
return s3Config
|
||||
}
|
||||
|
||||
@@ -24,10 +24,19 @@ import (
|
||||
"github.com/minio/minio/pkg/env"
|
||||
)
|
||||
|
||||
// Port mcs default port
|
||||
var Port = "9090"
|
||||
|
||||
// Hostname mcs hostname
|
||||
var Hostname = "localhost"
|
||||
|
||||
// TLSHostname mcs tls hostname
|
||||
var TLSHostname = "localhost"
|
||||
|
||||
// TLSPort mcs tls port
|
||||
var TLSPort = "9443"
|
||||
|
||||
// TLSRedirect mcs tls redirect rule
|
||||
var TLSRedirect = "off"
|
||||
|
||||
func getAccessKey() string {
|
||||
@@ -39,7 +48,17 @@ func getSecretKey() string {
|
||||
}
|
||||
|
||||
func getMinIOServer() string {
|
||||
return env.Get(McsMinIOServer, "http://localhost:9000")
|
||||
return strings.TrimSpace(env.Get(McsMinIOServer, "http://localhost:9000"))
|
||||
}
|
||||
|
||||
// If MCS_MINIO_SERVER_TLS_ROOT_CAS is true mcs will load a list of certificates into the
|
||||
// http.client rootCAs store, this is useful for testing or when working with self-signed certificates
|
||||
func getMinioServerTLSRootCAs() []string {
|
||||
caCertFileNames := strings.TrimSpace(env.Get(McsMinIOServerTLSRootCAs, ""))
|
||||
if caCertFileNames == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(caCertFileNames, ",")
|
||||
}
|
||||
|
||||
func getMinIOEndpoint() string {
|
||||
@@ -58,7 +77,7 @@ func getMinIOEndpointIsSecure() bool {
|
||||
if strings.Contains(server, "://") {
|
||||
parts := strings.Split(server, "://")
|
||||
if len(parts) > 1 {
|
||||
if parts[1] == "https" {
|
||||
if parts[0] == "https" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -70,10 +89,14 @@ func getProductionMode() bool {
|
||||
return strings.ToLower(env.Get(McsProductionMode, "on")) == "on"
|
||||
}
|
||||
|
||||
// GetHostname gets mcs hostname set on env variable,
|
||||
// default one or defined on run command
|
||||
func GetHostname() string {
|
||||
return strings.ToLower(env.Get(McsHostname, Hostname))
|
||||
}
|
||||
|
||||
// GetPort gets mcs por set on env variable
|
||||
// or default one
|
||||
func GetPort() int {
|
||||
port, err := strconv.Atoi(env.Get(McsPort, Port))
|
||||
if err != nil {
|
||||
@@ -82,10 +105,14 @@ func GetPort() int {
|
||||
return port
|
||||
}
|
||||
|
||||
// GetSSLHostname gets mcs ssl hostname set on env variable
|
||||
// or default one
|
||||
func GetSSLHostname() string {
|
||||
return strings.ToLower(env.Get(McsTLSHostname, TLSHostname))
|
||||
}
|
||||
|
||||
// GetSSLPort gets mcs ssl port set on env variable
|
||||
// or default one
|
||||
func GetSSLPort() int {
|
||||
port, err := strconv.Atoi(env.Get(McsTLSPort, TLSPort))
|
||||
if err != nil {
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
"github.com/minio/mcs/models"
|
||||
"github.com/minio/mcs/pkg/auth"
|
||||
|
||||
assetfs "github.com/elazarl/go-bindata-assetfs"
|
||||
assetFS "github.com/elazarl/go-bindata-assetfs"
|
||||
|
||||
portalUI "github.com/minio/mcs/portal-ui"
|
||||
|
||||
@@ -167,11 +167,44 @@ func FileServerMiddleware(next http.Handler) http.Handler {
|
||||
case strings.HasPrefix(r.URL.Path, "/api"):
|
||||
next.ServeHTTP(w, r)
|
||||
default:
|
||||
http.FileServer(&assetfs.AssetFS{
|
||||
assets := assetFS.AssetFS{
|
||||
Asset: portalUI.Asset,
|
||||
AssetDir: portalUI.AssetDir,
|
||||
AssetInfo: portalUI.AssetInfo,
|
||||
Prefix: "build"}).ServeHTTP(w, r)
|
||||
Prefix: "build"}
|
||||
wrapHandlerSinglePageApplication(http.FileServer(&assets)).ServeHTTP(w, r)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type notFoundRedirectRespWr struct {
|
||||
http.ResponseWriter // We embed http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (w *notFoundRedirectRespWr) WriteHeader(status int) {
|
||||
w.status = status // Store the status for our own use
|
||||
if status != http.StatusNotFound {
|
||||
w.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *notFoundRedirectRespWr) Write(p []byte) (int, error) {
|
||||
if w.status != http.StatusNotFound {
|
||||
return w.ResponseWriter.Write(p)
|
||||
}
|
||||
return len(p), nil // Lie that we successfully wrote it
|
||||
}
|
||||
|
||||
// wrapHandlerSinglePageApplication handles a http.FileServer returning a 404 and overrides it with index.html
|
||||
func wrapHandlerSinglePageApplication(h http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
nfrw := ¬FoundRedirectRespWr{ResponseWriter: w}
|
||||
h.ServeHTTP(nfrw, r)
|
||||
if nfrw.status == 404 {
|
||||
log.Printf("Redirecting %s to index.html.", r.RequestURI)
|
||||
http.Redirect(w, r, "/index.html", http.StatusFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,16 @@ package restapi
|
||||
|
||||
const (
|
||||
// consts for common configuration
|
||||
McsVersion = `0.1.0`
|
||||
McsAccessKey = "MCS_ACCESS_KEY"
|
||||
McsSecretKey = "MCS_SECRET_KEY"
|
||||
McsMinIOServer = "MCS_MINIO_SERVER"
|
||||
McsProductionMode = "MCS_PRODUCTION_MODE"
|
||||
McsHostname = "MCS_HOSTNAME"
|
||||
McsPort = "MCS_PORT"
|
||||
McsTLSHostname = "MCS_TLS_HOSTNAME"
|
||||
McsTLSPort = "MCS_TLS_PORT"
|
||||
McsVersion = `0.1.0`
|
||||
McsAccessKey = "MCS_ACCESS_KEY"
|
||||
McsSecretKey = "MCS_SECRET_KEY"
|
||||
McsMinIOServer = "MCS_MINIO_SERVER"
|
||||
McsMinIOServerTLSRootCAs = "MCS_MINIO_SERVER_TLS_ROOT_CAS"
|
||||
McsProductionMode = "MCS_PRODUCTION_MODE"
|
||||
McsHostname = "MCS_HOSTNAME"
|
||||
McsPort = "MCS_PORT"
|
||||
McsTLSHostname = "MCS_TLS_HOSTNAME"
|
||||
McsTLSPort = "MCS_TLS_PORT"
|
||||
|
||||
// consts for Secure middleware
|
||||
McsSecureAllowedHosts = "MCS_SECURE_ALLOWED_HOSTS"
|
||||
|
||||
@@ -993,6 +993,41 @@ func init() {
|
||||
}
|
||||
},
|
||||
"/service-accounts": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
],
|
||||
"summary": "List User's Service Accounts",
|
||||
"operationId": "ListUserServiceAccounts",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/serviceAccounts"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Generic error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
@@ -1005,7 +1040,7 @@ func init() {
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/serviceAccount"
|
||||
"$ref": "#/definitions/serviceAccountRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1025,6 +1060,34 @@ func init() {
|
||||
}
|
||||
}
|
||||
},
|
||||
"/service-accounts/{access_key}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
],
|
||||
"summary": "Delete Service Account",
|
||||
"operationId": "DeleteServiceAccount",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "access_key",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "A successful response."
|
||||
},
|
||||
"default": {
|
||||
"description": "Generic error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/service/restart": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -1883,15 +1946,6 @@ func init() {
|
||||
}
|
||||
}
|
||||
},
|
||||
"serviceAccount": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"policy": {
|
||||
"type": "string",
|
||||
"title": "policy to be applied to the Service Account if any"
|
||||
}
|
||||
}
|
||||
},
|
||||
"serviceAccountCreds": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1903,9 +1957,30 @@ func init() {
|
||||
}
|
||||
}
|
||||
},
|
||||
"serviceAccountRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"policy": {
|
||||
"type": "string",
|
||||
"title": "policy to be applied to the Service Account if any"
|
||||
}
|
||||
}
|
||||
},
|
||||
"serviceAccounts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sessionResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -3033,6 +3108,41 @@ func init() {
|
||||
}
|
||||
},
|
||||
"/service-accounts": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
],
|
||||
"summary": "List User's Service Accounts",
|
||||
"operationId": "ListUserServiceAccounts",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/serviceAccounts"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Generic error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
@@ -3045,7 +3155,7 @@ func init() {
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/serviceAccount"
|
||||
"$ref": "#/definitions/serviceAccountRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -3065,6 +3175,34 @@ func init() {
|
||||
}
|
||||
}
|
||||
},
|
||||
"/service-accounts/{access_key}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"UserAPI"
|
||||
],
|
||||
"summary": "Delete Service Account",
|
||||
"operationId": "DeleteServiceAccount",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "access_key",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "A successful response."
|
||||
},
|
||||
"default": {
|
||||
"description": "Generic error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/service/restart": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -3923,15 +4061,6 @@ func init() {
|
||||
}
|
||||
}
|
||||
},
|
||||
"serviceAccount": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"policy": {
|
||||
"type": "string",
|
||||
"title": "policy to be applied to the Service Account if any"
|
||||
}
|
||||
}
|
||||
},
|
||||
"serviceAccountCreds": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3943,9 +4072,30 @@ func init() {
|
||||
}
|
||||
}
|
||||
},
|
||||
"serviceAccountRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"policy": {
|
||||
"type": "string",
|
||||
"title": "policy to be applied to the Service Account if any"
|
||||
}
|
||||
}
|
||||
},
|
||||
"serviceAccounts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sessionResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -105,6 +105,9 @@ func NewMcsAPI(spec *loads.Document) *McsAPI {
|
||||
UserAPIDeleteBucketEventHandler: user_api.DeleteBucketEventHandlerFunc(func(params user_api.DeleteBucketEventParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation user_api.DeleteBucketEvent has not yet been implemented")
|
||||
}),
|
||||
UserAPIDeleteServiceAccountHandler: user_api.DeleteServiceAccountHandlerFunc(func(params user_api.DeleteServiceAccountParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation user_api.DeleteServiceAccount has not yet been implemented")
|
||||
}),
|
||||
AdminAPIGetUserInfoHandler: admin_api.GetUserInfoHandlerFunc(func(params admin_api.GetUserInfoParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation admin_api.GetUserInfo has not yet been implemented")
|
||||
}),
|
||||
@@ -126,6 +129,9 @@ func NewMcsAPI(spec *loads.Document) *McsAPI {
|
||||
AdminAPIListPoliciesHandler: admin_api.ListPoliciesHandlerFunc(func(params admin_api.ListPoliciesParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation admin_api.ListPolicies has not yet been implemented")
|
||||
}),
|
||||
UserAPIListUserServiceAccountsHandler: user_api.ListUserServiceAccountsHandlerFunc(func(params user_api.ListUserServiceAccountsParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation user_api.ListUserServiceAccounts has not yet been implemented")
|
||||
}),
|
||||
AdminAPIListUsersHandler: admin_api.ListUsersHandlerFunc(func(params admin_api.ListUsersParams, principal *models.Principal) middleware.Responder {
|
||||
return middleware.NotImplemented("operation admin_api.ListUsers has not yet been implemented")
|
||||
}),
|
||||
@@ -263,6 +269,8 @@ type McsAPI struct {
|
||||
UserAPIDeleteBucketHandler user_api.DeleteBucketHandler
|
||||
// UserAPIDeleteBucketEventHandler sets the operation handler for the delete bucket event operation
|
||||
UserAPIDeleteBucketEventHandler user_api.DeleteBucketEventHandler
|
||||
// UserAPIDeleteServiceAccountHandler sets the operation handler for the delete service account operation
|
||||
UserAPIDeleteServiceAccountHandler user_api.DeleteServiceAccountHandler
|
||||
// AdminAPIGetUserInfoHandler sets the operation handler for the get user info operation
|
||||
AdminAPIGetUserInfoHandler admin_api.GetUserInfoHandler
|
||||
// AdminAPIGroupInfoHandler sets the operation handler for the group info operation
|
||||
@@ -277,6 +285,8 @@ type McsAPI struct {
|
||||
AdminAPIListGroupsHandler admin_api.ListGroupsHandler
|
||||
// AdminAPIListPoliciesHandler sets the operation handler for the list policies operation
|
||||
AdminAPIListPoliciesHandler admin_api.ListPoliciesHandler
|
||||
// UserAPIListUserServiceAccountsHandler sets the operation handler for the list user service accounts operation
|
||||
UserAPIListUserServiceAccountsHandler user_api.ListUserServiceAccountsHandler
|
||||
// AdminAPIListUsersHandler sets the operation handler for the list users operation
|
||||
AdminAPIListUsersHandler admin_api.ListUsersHandler
|
||||
// UserAPILoginHandler sets the operation handler for the login operation
|
||||
@@ -432,6 +442,9 @@ func (o *McsAPI) Validate() error {
|
||||
if o.UserAPIDeleteBucketEventHandler == nil {
|
||||
unregistered = append(unregistered, "user_api.DeleteBucketEventHandler")
|
||||
}
|
||||
if o.UserAPIDeleteServiceAccountHandler == nil {
|
||||
unregistered = append(unregistered, "user_api.DeleteServiceAccountHandler")
|
||||
}
|
||||
if o.AdminAPIGetUserInfoHandler == nil {
|
||||
unregistered = append(unregistered, "admin_api.GetUserInfoHandler")
|
||||
}
|
||||
@@ -453,6 +466,9 @@ func (o *McsAPI) Validate() error {
|
||||
if o.AdminAPIListPoliciesHandler == nil {
|
||||
unregistered = append(unregistered, "admin_api.ListPoliciesHandler")
|
||||
}
|
||||
if o.UserAPIListUserServiceAccountsHandler == nil {
|
||||
unregistered = append(unregistered, "user_api.ListUserServiceAccountsHandler")
|
||||
}
|
||||
if o.AdminAPIListUsersHandler == nil {
|
||||
unregistered = append(unregistered, "admin_api.ListUsersHandler")
|
||||
}
|
||||
@@ -669,6 +685,10 @@ func (o *McsAPI) initHandlerCache() {
|
||||
o.handlers["DELETE"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["DELETE"]["/buckets/{bucket_name}/events/{arn}"] = user_api.NewDeleteBucketEvent(o.context, o.UserAPIDeleteBucketEventHandler)
|
||||
if o.handlers["DELETE"] == nil {
|
||||
o.handlers["DELETE"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["DELETE"]["/service-accounts/{access_key}"] = user_api.NewDeleteServiceAccount(o.context, o.UserAPIDeleteServiceAccountHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
@@ -700,6 +720,10 @@ func (o *McsAPI) initHandlerCache() {
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/service-accounts"] = user_api.NewListUserServiceAccounts(o.context, o.UserAPIListUserServiceAccountsHandler)
|
||||
if o.handlers["GET"] == nil {
|
||||
o.handlers["GET"] = make(map[string]http.Handler)
|
||||
}
|
||||
o.handlers["GET"]["/users"] = admin_api.NewListUsers(o.context, o.AdminAPIListUsersHandler)
|
||||
if o.handlers["POST"] == nil {
|
||||
o.handlers["POST"] = make(map[string]http.Handler)
|
||||
|
||||
@@ -53,7 +53,7 @@ type CreateServiceAccountParams struct {
|
||||
Required: true
|
||||
In: body
|
||||
*/
|
||||
Body *models.ServiceAccount
|
||||
Body *models.ServiceAccountRequest
|
||||
}
|
||||
|
||||
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
|
||||
@@ -67,7 +67,7 @@ func (o *CreateServiceAccountParams) BindRequest(r *http.Request, route *middlew
|
||||
|
||||
if runtime.HasBody(r) {
|
||||
defer r.Body.Close()
|
||||
var body models.ServiceAccount
|
||||
var body models.ServiceAccountRequest
|
||||
if err := route.Consumer.Consume(r.Body, &body); err != nil {
|
||||
if err == io.EOF {
|
||||
res = append(res, errors.Required("body", "body"))
|
||||
|
||||
90
restapi/operations/user_api/delete_service_account.go
Normal file
90
restapi/operations/user_api/delete_service_account.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the generate command
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
|
||||
"github.com/minio/mcs/models"
|
||||
)
|
||||
|
||||
// DeleteServiceAccountHandlerFunc turns a function with the right signature into a delete service account handler
|
||||
type DeleteServiceAccountHandlerFunc func(DeleteServiceAccountParams, *models.Principal) middleware.Responder
|
||||
|
||||
// Handle executing the request and returning a response
|
||||
func (fn DeleteServiceAccountHandlerFunc) Handle(params DeleteServiceAccountParams, principal *models.Principal) middleware.Responder {
|
||||
return fn(params, principal)
|
||||
}
|
||||
|
||||
// DeleteServiceAccountHandler interface for that can handle valid delete service account params
|
||||
type DeleteServiceAccountHandler interface {
|
||||
Handle(DeleteServiceAccountParams, *models.Principal) middleware.Responder
|
||||
}
|
||||
|
||||
// NewDeleteServiceAccount creates a new http.Handler for the delete service account operation
|
||||
func NewDeleteServiceAccount(ctx *middleware.Context, handler DeleteServiceAccountHandler) *DeleteServiceAccount {
|
||||
return &DeleteServiceAccount{Context: ctx, Handler: handler}
|
||||
}
|
||||
|
||||
/*DeleteServiceAccount swagger:route DELETE /service-accounts/{access_key} UserAPI deleteServiceAccount
|
||||
|
||||
Delete Service Account
|
||||
|
||||
*/
|
||||
type DeleteServiceAccount struct {
|
||||
Context *middleware.Context
|
||||
Handler DeleteServiceAccountHandler
|
||||
}
|
||||
|
||||
func (o *DeleteServiceAccount) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
route, rCtx, _ := o.Context.RouteInfo(r)
|
||||
if rCtx != nil {
|
||||
r = rCtx
|
||||
}
|
||||
var Params = NewDeleteServiceAccountParams()
|
||||
|
||||
uprinc, aCtx, err := o.Context.Authorize(r, route)
|
||||
if err != nil {
|
||||
o.Context.Respond(rw, r, route.Produces, route, err)
|
||||
return
|
||||
}
|
||||
if aCtx != nil {
|
||||
r = aCtx
|
||||
}
|
||||
var principal *models.Principal
|
||||
if uprinc != nil {
|
||||
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
|
||||
}
|
||||
|
||||
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
|
||||
o.Context.Respond(rw, r, route.Produces, route, err)
|
||||
return
|
||||
}
|
||||
|
||||
res := o.Handler.Handle(Params, principal) // actually handle the request
|
||||
|
||||
o.Context.Respond(rw, r, route.Produces, route, res)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package user_api
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/go-openapi/strfmt"
|
||||
)
|
||||
|
||||
// NewDeleteServiceAccountParams creates a new DeleteServiceAccountParams object
|
||||
// no default values defined in spec.
|
||||
func NewDeleteServiceAccountParams() DeleteServiceAccountParams {
|
||||
|
||||
return DeleteServiceAccountParams{}
|
||||
}
|
||||
|
||||
// DeleteServiceAccountParams contains all the bound params for the delete service account operation
|
||||
// typically these are obtained from a http.Request
|
||||
//
|
||||
// swagger:parameters DeleteServiceAccount
|
||||
type DeleteServiceAccountParams struct {
|
||||
|
||||
// HTTP Request Object
|
||||
HTTPRequest *http.Request `json:"-"`
|
||||
|
||||
/*
|
||||
Required: true
|
||||
In: path
|
||||
*/
|
||||
AccessKey string
|
||||
}
|
||||
|
||||
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
|
||||
// for simple values it will use straight method calls.
|
||||
//
|
||||
// To ensure default values, the struct must have been initialized with NewDeleteServiceAccountParams() beforehand.
|
||||
func (o *DeleteServiceAccountParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
|
||||
var res []error
|
||||
|
||||
o.HTTPRequest = r
|
||||
|
||||
rAccessKey, rhkAccessKey, _ := route.Params.GetOK("access_key")
|
||||
if err := o.bindAccessKey(rAccessKey, rhkAccessKey, route.Formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// bindAccessKey binds and validates parameter AccessKey from path.
|
||||
func (o *DeleteServiceAccountParams) bindAccessKey(rawData []string, hasKey bool, formats strfmt.Registry) error {
|
||||
var raw string
|
||||
if len(rawData) > 0 {
|
||||
raw = rawData[len(rawData)-1]
|
||||
}
|
||||
|
||||
// Required: true
|
||||
// Parameter is provided by construction from the route
|
||||
|
||||
o.AccessKey = raw
|
||||
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user