Implemented Log Search API & Prometheus functionality (#549)

Implemented Log Search API & Prometheus functionality in console, also fixed minor issues in all the platform

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2021-01-13 14:08:32 -06:00
committed by GitHub
parent f3bcfc327d
commit 1c109769df
78 changed files with 6753 additions and 714 deletions

View File

@@ -4,3 +4,4 @@ target/
console
!console/
portal-ui/node_modules/
.git/

5
.gitignore vendored
View File

@@ -24,6 +24,11 @@ console
dist/
# Ignore node_modules
portal-ui/node_modules/
portal-ui/build/
# Ignore tls cert and key
private.key
public.crt

View File

@@ -33,7 +33,7 @@ RUN go mod download
ADD . /go/src/github.com/minio/console/
WORKDIR /go/src/github.com/minio/console/
COPY --from=uilayer /app/bindata_assetfs.go /go/src/github.com/minio/console/portal-ui/
COPY --from=uilayer /app/bindata_assetfs.go /go/src/github.com/minio/console/portal-ui/bindata_assetfs.go
ENV CGO_ENABLED=0

View File

@@ -31,7 +31,6 @@ import (
var (
errCantDetermineMinIOImage = errors.New("can't determine MinIO Image")
errCantDetermineMCImage = errors.New("can't determine MC Image")
)
func GetK8sAPIServer() string {
@@ -120,44 +119,3 @@ func GetLatestMinioImage(client HTTPClientI) (*string, error) {
}
return latestMinIOImage, nil
}
// getLatestMCImage returns the latest docker image for MC if found on the internet
func getLatestMCImage() (*string, error) {
// Create an http client with a 4 second timeout
client := http.Client{
Timeout: 4 * time.Second,
}
resp, err := client.Get("https://dl.min.io/client/mc/release/linux-amd64/")
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var re = regexp.MustCompile(`(?m)\.\/mc\.(RELEASE.*?Z)"`)
// look for a single match
matches := re.FindAllStringSubmatch(string(body), 1)
for i := range matches {
release := matches[i][1]
dockerImage := fmt.Sprintf("minio/mc:%s", release)
return &dockerImage, nil
}
return nil, errCantDetermineMCImage
}
var latestMCImage, errLatestMCImage = getLatestMCImage()
func GetMCImage() (*string, error) {
image := strings.TrimSpace(env.Get(ConsoleMCImage, ""))
// if there is a preferred image configured by the user we'll always return that
if image != "" {
return &image, nil
}
if errLatestMCImage != nil {
return nil, errLatestMCImage
}
return latestMCImage, nil
}

5
go.mod
View File

@@ -20,6 +20,7 @@ require (
github.com/minio/minio v0.0.0-20201221162327-6df6ac0f3410
github.com/minio/minio-go/v7 v7.0.7-0.20201217170524-3baf9ea06f7c
github.com/minio/operator v0.0.0-20201204220226-9901d1d0766c
github.com/minio/operator/logsearchapi v0.0.0-20201217190212-bf6546b09012
github.com/mitchellh/go-homedir v1.1.0
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/secure-io/sio-go v0.3.1
@@ -30,6 +31,8 @@ require (
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
gopkg.in/yaml.v2 v2.3.0
k8s.io/api v0.18.6
k8s.io/apimachinery v0.18.6
k8s.io/apimachinery v0.18.8
k8s.io/client-go v0.18.6
)
replace github.com/minio/operator v0.0.0-20201204220226-9901d1d0766c => github.com/dvaldivia/operator v0.0.0-20201230052356-04efc0ea5890

159
go.sum
View File

@@ -123,6 +123,7 @@ github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUW
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
github.com/alecthomas/participle v0.2.1 h1:4AVLj1viSGa4LG5HDXKXrm5xRx19SB/rS/skPQB1Grw=
github.com/alecthomas/participle v0.2.1/go.mod h1:SW6HZGeZgSIpcUWX3fXpfZhuaWHnmoD5KCVaqSaNTkk=
@@ -131,6 +132,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ=
@@ -165,6 +168,7 @@ github.com/aws/aws-sdk-go v1.31.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU
github.com/aws/aws-sdk-go v1.35.20/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2/go.mod h1:RDu/qcrnpEdJC/p8tx34+YBFqqX71lB7dOX9QE+ZC4M=
github.com/bcicen/jstream v1.0.1 h1:BXY7Cu4rdmc0rhyTVyT3UkxAiX3bnLpKLas9btbH5ck=
github.com/bcicen/jstream v1.0.1/go.mod h1:9ielPxqFry7Y4Tg3j4BfjPocfJ3TbsRtXOAYXYmRuAQ=
github.com/beevik/ntp v0.3.0 h1:xzVrPrE4ziasFXgBVBZJDP0Wg/KpMwk2KHJ4Ba8GrDw=
@@ -194,6 +198,7 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheggaaa/pb v1.0.28/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo=
github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -204,6 +209,10 @@ github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/cockroach-go/v2 v2.0.3 h1:ZA346ACHIZctef6trOTwBAEvPVm1k0uLm/bb2Atc+S8=
github.com/cockroachdb/cockroach-go/v2 v2.0.3/go.mod h1:hAuDgiVgDVkfirP9JnhXEfcXEPRKBpYdGz+l7mvYSzw=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
@@ -227,6 +236,9 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
@@ -242,6 +254,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4=
github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
@@ -268,6 +281,8 @@ github.com/dswarbrick/smart v0.0.0-20190505152634-909a45200d6d/go.mod h1:apXo4PA
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvaldivia/operator v0.0.0-20201230052356-04efc0ea5890 h1:vfbzHXeky4e8EOS40YPSLNCxNi7KkevPB7hi5tu/O9g=
github.com/dvaldivia/operator v0.0.0-20201230052356-04efc0ea5890/go.mod h1:EmTYFyr2nk12P9afUXPPsCKbNhYDruQ/PuZtDenESH0=
github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q=
@@ -291,8 +306,12 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE=
github.com/evanphx/json-patch v0.0.0-20200808040245-162e5629780b/go.mod h1:NAJj0yf/KaRKURN6nyi7A9IZydMivZEm9oQLWNjfKDc=
github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I=
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.1.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
@@ -312,6 +331,8 @@ github.com/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/georgysavva/scany v0.2.7 h1:SBEuurTvWOUp7FnGBOjeSF9XWaWmVzc91h9baPo6y2s=
github.com/georgysavva/scany v0.2.7/go.mod h1:bcxPhzeQFQqAUmjlZVwTGlu6AnWFSOiHpalfBe0xQ6U=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
@@ -367,6 +388,7 @@ github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf
github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs=
github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI=
github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk=
github.com/go-openapi/loads v0.19.5 h1:jZVYWawIQiA1NBnHla28ktg6hrcfTHsCE+3QLVRBIls=
github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY=
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
@@ -402,6 +424,7 @@ github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfT
github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo=
github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
github.com/go-openapi/validate v0.19.10 h1:tG3SZ5DC5KF4cyt7nqLVcQXGj5A7mpaYkAcNPlDK+Yk=
github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@@ -434,6 +457,7 @@ github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSC
github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=
@@ -451,7 +475,10 @@ github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGt
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -459,6 +486,7 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -676,6 +704,74 @@ github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7V
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.7.0 h1:pwjzcYyfmz/HQOQlENvG1OcDqauTGaqlVahq934F0/U=
github.com/jackc/pgconn v1.7.0/go.mod h1:sF/lPpNEMEOp+IYhyQGdAvrG20gWf6A1tKlr0v7JMeA=
github.com/jackc/pgconn v1.8.0 h1:FmjZ0rOyXTr1wfWs45i4a9vjnjWUAGpMuQLD9OSs+lw=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.5 h1:NUbEWPmCQZbMmYlTjVoNPhc0CfnYyz2bfUAh6A5ZVJM=
github.com/jackc/pgproto3/v2 v2.0.5/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.6 h1:b1105ZGEMFe7aCvrT1Cca3VoVb4ZFMaFJLJcg/3zD+8=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
github.com/jackc/pgtype v1.3.0/go.mod h1:b0JqxHvPmljG+HQ5IsvQ0yqeSi4nGcDTVjFoiLDb0Ik=
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.3.1-0.20200612023650-09efc3839047/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
github.com/jackc/pgtype v1.5.0 h1:jzBqRk2HFG2CV4AIwgCI2PwTgm6UUoCAK2ofHHRirtc=
github.com/jackc/pgtype v1.5.0/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
github.com/jackc/pgtype v1.6.2 h1:b3pDeuhbbzBYcg5kwNmNDun4pFUD/0AAr1kLXZLeNt8=
github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
github.com/jackc/pgx/v4 v4.6.0/go.mod h1:vPh43ZzxijXUVJ+t/EmXBtFmbFVO72cuneCT9oAlxAg=
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
github.com/jackc/pgx/v4 v4.9.0 h1:6STjDqppM2ROy5p1wNDcsC7zJTjSHeuCsguZmXyzx7c=
github.com/jackc/pgx/v4 v4.9.0/go.mod h1:MNGWmViCgqbZck9ujOOBN63gK9XVGILXWCvKLGKmnms=
github.com/jackc/pgx/v4 v4.10.0 h1:xXTl+lSiF1eFQ4U7vUL493n/1q8ZhSDP962rSKhgRZo=
github.com/jackc/pgx/v4 v4.10.0/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.2 h1:mpQEXihFnWGDy6X98EOTh81JYuxn7txby8ilJ3iIPGM=
github.com/jackc/puddle v1.1.2/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3 h1:JnPg/5Q9xVJGfjsO5CPUOjnJps1JaRUm8I9FXVCFK94=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jcmturner/gofork v0.0.0-20180107083740-2aebee971930/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
@@ -684,6 +780,9 @@ github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/U
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s=
github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@@ -691,6 +790,7 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
@@ -724,6 +824,7 @@ github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.10.1/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.3 h1:dB4Bn0tN3wdCzQxnS8r06kV74qN/TAfaIS0bVE8h3jc=
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
@@ -734,6 +835,7 @@ github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo
github.com/klauspost/cpuid v1.2.4/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/readahead v1.3.1 h1:QqXNYvm+VvqYcbrRT4LojUciM0XrznFRIDrbHiJtu/0=
@@ -756,8 +858,11 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
@@ -795,7 +900,9 @@ github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
@@ -806,6 +913,7 @@ github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/Qd
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
@@ -825,16 +933,20 @@ github.com/minio/mc v0.0.0-20201220181029-41c804b179de h1:bm1FlYN5TSkB9/IpqREPkB
github.com/minio/mc v0.0.0-20201220181029-41c804b179de/go.mod h1:2nbeDAB4Bkges8QHq6yKoJnjHz3lYqUeHaLGFDUDUbY=
github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4=
github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
github.com/minio/minio v0.0.0-20201029200030-7331659d3ddc/go.mod h1:640kMkCwiyOX8dheptHYModdUw4HrnUFHKw3McqUXD8=
github.com/minio/minio v0.0.0-20201203193910-919441d9c4d2/go.mod h1:+wH6R6AjgNDUvMBb24p/e7zn8VU+ChUAXy4uhopGxQA=
github.com/minio/minio v0.0.0-20201219173637-3e16ec457a24/go.mod h1:sIgZV5KcIpn3bitFPXbrx/OyGcca57QzrgxT5I8nabg=
github.com/minio/minio v0.0.0-20201221162327-6df6ac0f3410 h1:2mM//tAaamqL1SmqYVb5rour6czx+mWhchs12goYT/w=
github.com/minio/minio v0.0.0-20201221162327-6df6ac0f3410/go.mod h1:sIgZV5KcIpn3bitFPXbrx/OyGcca57QzrgxT5I8nabg=
github.com/minio/minio-go/v7 v7.0.6-0.20200929220449-755b5633803a/go.mod h1:CSt2ETZNs+bIIhWTse0mcZKZWMGrFU7Er7RR0TmkDYk=
github.com/minio/minio-go/v7 v7.0.6 h1:9czXaG0LEZ9s74smSqy0rm034MxngQoP6HTTuSc5GEs=
github.com/minio/minio-go/v7 v7.0.6/go.mod h1:HcIuq+11d/3MfavIPZiswSzfQ1VJ2Lwxp/XLtW46IWQ=
github.com/minio/minio-go/v7 v7.0.7-0.20201217170524-3baf9ea06f7c h1:NgTbI1w/B+2Jcl+YKTULAAXqkwWqMZbkzmVdWNwzKnA=
github.com/minio/minio-go/v7 v7.0.7-0.20201217170524-3baf9ea06f7c/go.mod h1:pEZBUa+L2m9oECoIA6IcSK8bv/qggtQVLovjeKK5jYc=
github.com/minio/operator v0.0.0-20201204220226-9901d1d0766c h1:2QpnenH2gieq5yVh6sZYylXKCoBgwKkxcgqkLr/fq9M=
github.com/minio/operator v0.0.0-20201204220226-9901d1d0766c/go.mod h1:Xnb44PIBZF/JCN4uXEEzf9vFwhnB9zXsQgVKU7GThiM=
github.com/minio/operator/logsearchapi v0.0.0-20201217190212-bf6546b09012 h1:UnFJL5tYkdtXPeWUoxGwDkpVoWRwT4Fs1SFmjbcNjls=
github.com/minio/operator/logsearchapi v0.0.0-20201217190212-bf6546b09012/go.mod h1:kpA0C8LRbfUGVzGSC+Px7WYRiczblwqiRnP0ENv0tCU=
github.com/minio/selfupdate v0.3.1 h1:BWEFSNnrZVMUWXbXIgLDNDjbejkmpAmZvy/nCz1HlEs=
github.com/minio/selfupdate v0.3.1/go.mod h1:b8ThJzzH7u2MkF6PcIra7KaXO9Khf6alWPvMSyTDCFM=
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
@@ -961,6 +1073,7 @@ github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bA
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
@@ -1045,6 +1158,8 @@ github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -1067,10 +1182,15 @@ github.com/securego/gosec v0.0.0-20200401082031-e946c8c39989/go.mod h1:i9l/TNj+y
github.com/securego/gosec/v2 v2.3.0/go.mod h1:UzeVyUXbxukhLeHKV3VVqo7HdoQR9MrRfFmZYotn8ME=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
github.com/shirou/gopsutil v2.20.3-0.20200314133625-53cec6b37e6a+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil v2.20.9+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil v3.20.11+incompatible h1:LJr4ZQK4mPpIV5gOa4jCOKOGb4ty4DZO54I4FGqIpto=
github.com/shirou/gopsutil v3.20.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v0.0.0-20200419222939-1884f454f8ea h1:jaXWVFZ98/ihXniiDzqNXQgMSgklX4kjfDWZTE3ZtdU=
github.com/shopspring/decimal v0.0.0-20200419222939-1884f454f8ea/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@@ -1127,6 +1247,7 @@ github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1Sd
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
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 v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -1150,6 +1271,7 @@ github.com/tidwall/sjson v1.0.4 h1:UcdIRXff12Lpnu3OLtZvnc03g4vH2suXDXhBwBqmzYg=
github.com/tidwall/sjson v1.0.4/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y=
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.3 h1:3giwAkmtaEDLSV0MdO1lDLuPgklgPzmk8H9+So2BVfA=
github.com/tinylib/msgp v1.1.3/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
@@ -1179,6 +1301,7 @@ github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOV
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/vdemeester/k8s-pkg-credentialprovider v1.17.4/go.mod h1:inCTmtUdr5KJbreVojo06krnTgaeAz/Z7lynpPk/Q2c=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU=
github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
@@ -1199,6 +1322,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
@@ -1207,8 +1331,10 @@ go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.etcd.io/etcd v0.0.0-20201125193152-8a03d2e9614b h1:5makfKENOTVu2bNoHzSqwwz+g70ivWLSnExzd33/2bI=
go.etcd.io/etcd v0.0.0-20201125193152-8a03d2e9614b/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=
go.etcd.io/etcd/v3 v3.3.0-rc.0.0.20200707003333-58bb8ae09f8e/go.mod h1:UENlOa05tkNvLx9VnNziSerG4Ro74upGK6Apd4v6M/Y=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE=
go.mongodb.org/mongo-driver v1.3.4 h1:zs/dKNwX0gYUtzwrN9lLiR15hCO0nDwQj5xXx+vjCdE=
go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE=
@@ -1232,6 +1358,7 @@ 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.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo=
@@ -1245,7 +1372,9 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/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=
@@ -1255,9 +1384,12 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
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-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/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/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -1400,6 +1532,7 @@ golang.org/x/sys v0.0.0-20190620070143-6f217b454f45/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1428,6 +1561,7 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200915084602-288bc346aa39/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1458,6 +1592,7 @@ golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190221204921-83362c3779f5/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -1473,6 +1608,7 @@ golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -1485,6 +1621,7 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20190719005602-e377ae9d6386/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -1525,9 +1662,12 @@ golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200502202811-ed308ab3e770/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200616195046-dc31b401abb5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200929223013-bf155c11ec6f h1:7+Nz9MyPqt2qMCTvNiRy1G0zYfkB7UCa+ayT6uVvbyI=
golang.org/x/tools v0.0.0-20200929223013-bf155c11ec6f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1640,6 +1780,7 @@ gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/h2non/filetype.v1 v1.0.5 h1:CC1jjJjoEhNVbMhXYalmGBhOBK2V70Q1N850wt/98/Y=
gopkg.in/h2non/filetype.v1 v1.0.5/go.mod h1:M0yem4rwSX5lLVrkEuRRp2/NinFMD5vgJ4DlAhZcfNo=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -1675,12 +1816,14 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
@@ -1696,28 +1839,41 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.5 h1:nI5egYTGJakVyOryqLs1cQO5dO0ksin5XXs2pspk75k=
honnef.co/go/tools v0.0.1-2020.1.5/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.17.4/go.mod h1:5qxx6vjmwUVG2nHQTKGlLts8Tbok8PzHl4vHtVFuZCA=
k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78=
k8s.io/api v0.18.6 h1:osqrAXbOQjkKIWDTjrqxWQ3w0GkKb1KA1XkUGHHYpeE=
k8s.io/api v0.18.6/go.mod h1:eeyxr+cwCjMdLAmr2W3RyDI0VvTawSg/3RFFBEnmZGI=
k8s.io/apiextensions-apiserver v0.18.2/go.mod h1:q3faSnRGmYimiocj6cHQ1I3WpLqmDgJFlKL37fC4ZvY=
k8s.io/apimachinery v0.17.4/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g=
k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
k8s.io/apimachinery v0.18.6 h1:RtFHnfGNfd1N0LeSrKCUznz5xtUP1elRGvHJbL3Ntag=
k8s.io/apimachinery v0.18.6/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
k8s.io/apimachinery v0.18.8 h1:jimPrycCqgx2QPearX3to1JePz7wSbVLq+7PdBTTwQ0=
k8s.io/apimachinery v0.18.8/go.mod h1:6sQd+iHEqmOtALqOFjSWp2KZ9F0wlU/nWm0ZgsYWMig=
k8s.io/apiserver v0.17.4/go.mod h1:5ZDQ6Xr5MNBxyi3iUZXS84QOhZl+W7Oq2us/29c0j9I=
k8s.io/apiserver v0.18.2/go.mod h1:Xbh066NqrZO8cbsoenCwyDJ1OSi8Ag8I2lezeHxzwzw=
k8s.io/client-go v0.17.4/go.mod h1:ouF6o5pz3is8qU0/qYL2RnoxOPqgfuidYLowytyLJmc=
k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU=
k8s.io/client-go v0.18.6 h1:I+oWqJbibLSGsZj8Xs8F0aWVXJVIoUHWaaJV3kUN/Zw=
k8s.io/client-go v0.18.6/go.mod h1:/fwtGLjYMS1MaM5oi+eXhKwG+1UHidUEXRh6cNsdO0Q=
k8s.io/cloud-provider v0.17.4/go.mod h1:XEjKDzfD+b9MTLXQFlDGkk6Ho8SGMpaU8Uugx/KNK9U=
k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s=
k8s.io/code-generator v0.18.2/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc=
k8s.io/component-base v0.17.4/go.mod h1:5BRqHMbbQPm2kKu35v3G+CpVq4K0RJKC7TRioF0I9lE=
k8s.io/component-base v0.18.2/go.mod h1:kqLlMuhJNHQ9lz8Z7V5bxUUtjFZnrypArGl58gmDfUM=
k8s.io/csi-translation-lib v0.17.4/go.mod h1:CsxmjwxEI0tTNMzffIAcgR9lX4wOh6AKHdxQrT7L0oo=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/klog/v2 v2.3.0 h1:WmkrnW7fdrm0/DMClc+HIxtftvxVIPAhlVwMQo5yLco=
k8s.io/klog/v2 v2.3.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ=
k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 h1:Oh3Mzx5pJ+yIumsAD0MOECPVeXsVot0UkiaCGVyfGQY=
k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
k8s.io/legacy-cloud-providers v0.17.4/go.mod h1:FikRNoD64ECjkxO36gkDgJeiQWwyZTuBkhu+yxOc1Js=
@@ -1738,6 +1894,9 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0=
sigs.k8s.io/controller-tools v0.4.1/go.mod h1:G9rHdZMVlBDocIxGkK3jHLWqcTMNvveypYJwrvYKjWU=
sigs.k8s.io/kind v0.9.0/go.mod h1:cxKQWwmbtRDzQ+RNKnR6gZG6fjbeTtItp5cGf+ww+1Y=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06 h1:zD2IemQ4LmOcAumeiyDWXKUI2SO0NYDe3H6QGvPOVgU=
sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18=

View File

@@ -23,6 +23,9 @@ package models
// Editing this file might prove futile when you re-run the swagger generate command
import (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
@@ -40,10 +43,47 @@ type AdminInfoResponse struct {
// usage
Usage int64 `json:"usage,omitempty"`
// widgets
Widgets []*Widget `json:"widgets"`
}
// Validate validates this admin info response
func (m *AdminInfoResponse) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateWidgets(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *AdminInfoResponse) validateWidgets(formats strfmt.Registry) error {
if swag.IsZero(m.Widgets) { // not required
return nil
}
for i := 0; i < len(m.Widgets); i++ {
if swag.IsZero(m.Widgets[i]) { // not required
continue
}
if m.Widgets[i] != nil {
if err := m.Widgets[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("widgets" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}

View File

@@ -63,6 +63,12 @@ type CreateTenantRequest struct {
// erasure coding parity
ErasureCodingParity int64 `json:"erasureCodingParity,omitempty"`
// expose console
ExposeConsole bool `json:"expose_console,omitempty"`
// expose minio
ExposeMinio bool `json:"expose_minio,omitempty"`
// idp
Idp *IdpConfiguration `json:"idp,omitempty"`

View File

@@ -0,0 +1,60 @@
// 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"
"github.com/go-openapi/swag"
)
// LogSearchResponse log search response
//
// swagger:model logSearchResponse
type LogSearchResponse struct {
// list of log search responses
Results interface{} `json:"results,omitempty"`
}
// Validate validates this log search response
func (m *LogSearchResponse) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *LogSearchResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *LogSearchResponse) UnmarshalBinary(b []byte) error {
var res LogSearchResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

103
models/result_target.go Normal file
View File

@@ -0,0 +1,103 @@
// 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 (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// ResultTarget result target
//
// swagger:model resultTarget
type ResultTarget struct {
// legend format
LegendFormat string `json:"legendFormat,omitempty"`
// result
Result []*WidgetResult `json:"result"`
// result type
ResultType string `json:"resultType,omitempty"`
}
// Validate validates this result target
func (m *ResultTarget) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateResult(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *ResultTarget) validateResult(formats strfmt.Registry) error {
if swag.IsZero(m.Result) { // not required
return nil
}
for i := 0; i < len(m.Result); i++ {
if swag.IsZero(m.Result[i]) { // not required
continue
}
if m.Result[i] != nil {
if err := m.Result[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("result" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *ResultTarget) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *ResultTarget) UnmarshalBinary(b []byte) error {
var res ResultTarget
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -50,6 +50,9 @@ type Tenant struct {
// enable prometheus
EnablePrometheus bool `json:"enable_prometheus,omitempty"`
// endpoints
Endpoints *TenantEndpoints `json:"endpoints,omitempty"`
// image
Image string `json:"image,omitempty"`
@@ -73,6 +76,10 @@ type Tenant struct {
func (m *Tenant) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateEndpoints(formats); err != nil {
res = append(res, err)
}
if err := m.validatePools(formats); err != nil {
res = append(res, err)
}
@@ -87,6 +94,24 @@ func (m *Tenant) Validate(formats strfmt.Registry) error {
return nil
}
func (m *Tenant) validateEndpoints(formats strfmt.Registry) error {
if swag.IsZero(m.Endpoints) { // not required
return nil
}
if m.Endpoints != nil {
if err := m.Endpoints.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("endpoints")
}
return err
}
}
return nil
}
func (m *Tenant) validatePools(formats strfmt.Registry) error {
if swag.IsZero(m.Pools) { // not required
@@ -147,3 +172,38 @@ func (m *Tenant) UnmarshalBinary(b []byte) error {
*m = res
return nil
}
// TenantEndpoints tenant endpoints
//
// swagger:model TenantEndpoints
type TenantEndpoints struct {
// console
Console string `json:"console,omitempty"`
// minio
Minio string `json:"minio,omitempty"`
}
// Validate validates this tenant endpoints
func (m *TenantEndpoints) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *TenantEndpoints) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *TenantEndpoints) UnmarshalBinary(b []byte) error {
var res TenantEndpoints
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

219
models/widget.go Normal file
View File

@@ -0,0 +1,219 @@
// 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 (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// Widget widget
//
// swagger:model widget
type Widget struct {
// options
Options *WidgetOptions `json:"options,omitempty"`
// targets
Targets []*ResultTarget `json:"targets"`
// title
Title string `json:"title,omitempty"`
// type
Type string `json:"type,omitempty"`
}
// Validate validates this widget
func (m *Widget) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateOptions(formats); err != nil {
res = append(res, err)
}
if err := m.validateTargets(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *Widget) validateOptions(formats strfmt.Registry) error {
if swag.IsZero(m.Options) { // not required
return nil
}
if m.Options != nil {
if err := m.Options.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("options")
}
return err
}
}
return nil
}
func (m *Widget) validateTargets(formats strfmt.Registry) error {
if swag.IsZero(m.Targets) { // not required
return nil
}
for i := 0; i < len(m.Targets); i++ {
if swag.IsZero(m.Targets[i]) { // not required
continue
}
if m.Targets[i] != nil {
if err := m.Targets[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("targets" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *Widget) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *Widget) UnmarshalBinary(b []byte) error {
var res Widget
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}
// WidgetOptions widget options
//
// swagger:model WidgetOptions
type WidgetOptions struct {
// reduce options
ReduceOptions *WidgetOptionsReduceOptions `json:"reduceOptions,omitempty"`
}
// Validate validates this widget options
func (m *WidgetOptions) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateReduceOptions(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *WidgetOptions) validateReduceOptions(formats strfmt.Registry) error {
if swag.IsZero(m.ReduceOptions) { // not required
return nil
}
if m.ReduceOptions != nil {
if err := m.ReduceOptions.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("options" + "." + "reduceOptions")
}
return err
}
}
return nil
}
// MarshalBinary interface implementation
func (m *WidgetOptions) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *WidgetOptions) UnmarshalBinary(b []byte) error {
var res WidgetOptions
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}
// WidgetOptionsReduceOptions widget options reduce options
//
// swagger:model WidgetOptionsReduceOptions
type WidgetOptionsReduceOptions struct {
// calcs
Calcs []string `json:"calcs"`
}
// Validate validates this widget options reduce options
func (m *WidgetOptionsReduceOptions) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *WidgetOptionsReduceOptions) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *WidgetOptionsReduceOptions) UnmarshalBinary(b []byte) error {
var res WidgetOptionsReduceOptions
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

63
models/widget_result.go Normal file
View File

@@ -0,0 +1,63 @@
// 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"
"github.com/go-openapi/swag"
)
// WidgetResult widget result
//
// swagger:model widgetResult
type WidgetResult struct {
// metric
Metric map[string]string `json:"metric,omitempty"`
// values
Values []interface{} `json:"values"`
}
// Validate validates this widget result
func (m *WidgetResult) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *WidgetResult) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *WidgetResult) UnmarshalBinary(b []byte) error {
var res WidgetResult
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

File diff suppressed because one or more lines are too long

View File

@@ -3,9 +3,11 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@date-io/moment": "1.x",
"@hot-loader/react-dom": "17.0.1",
"@material-ui/core": "^4.9.12",
"@material-ui/icons": "^4.9.1",
"@material-ui/pickers": "^3.2.10",
"@types/history": "^4.7.3",
"@types/jest": "24.0.23",
"@types/lodash": "^4.14.149",
@@ -13,6 +15,7 @@
"@types/react": "17.0.0",
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-dom": "16.9.4",
"@types/react-grid-layout": "^1.1.1",
"@types/react-redux": "^7.1.5",
"@types/react-router": "^5.1.3",
"@types/react-router-dom": "^5.1.2",
@@ -27,7 +30,7 @@
"history": "^4.10.1",
"local-storage-fallback": "^4.1.1",
"lodash": "^4.17.19",
"moment": "^2.24.0",
"moment": "^2.29.1",
"react": "17.0.1",
"react-app-rewire-hot-loader": "^2.0.1",
"react-app-rewired": "^2.1.6",
@@ -36,6 +39,7 @@
"react-codemirror2": "^7.1.0",
"react-copy-to-clipboard": "^5.0.2",
"react-dom": "17.0.1",
"react-grid-layout": "^1.2.0",
"react-hot-loader": "^4.13.0",
"react-moment": "^0.9.7",
"react-redux": "^7.1.3",

View File

@@ -58,6 +58,8 @@ export interface ITenantCreator {
secret_key: string;
image: string;
console_image: string;
expose_minio: boolean;
expose_console: boolean;
pools: IPoolModel[];
namespace: string;
erasureCodingParity: number;

View File

@@ -39,7 +39,7 @@ export const units = [
export const k8sUnits = ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"];
export const k8sCalcUnits = ["B", ...k8sUnits];
export const niceBytes = (x: string) => {
export const niceBytes = (x: string, showK8sUnits: boolean = false) => {
let l = 0,
n = parseInt(x, 10) || 0;
@@ -48,7 +48,12 @@ export const niceBytes = (x: string) => {
}
//include a decimal point and a tenths-place digit if presenting
//less than ten of KB or greater units
return n.toFixed(n < 10 && l > 0 ? 1 : 0) + " " + units[l];
const k8sUnitsN = ["B", ...k8sUnits];
return (
n.toFixed(n < 10 && l > 0 ? 1 : 0) +
" " +
(showK8sUnits ? k8sUnitsN[l] : units[l])
);
};
export const setCookie = (name: string, val: string) => {
@@ -419,3 +424,122 @@ export const generatePoolName = (pools: IPoolModel[]) => {
return `pool-${poolCounter}`;
};
// seconds / minutes /hours / Days / Years calculator
export const niceDays = (secondsValue: string) => {
let seconds = parseFloat(secondsValue);
const days = Math.floor(seconds / (3600 * 24));
seconds -= days * 3600 * 24;
const hours = Math.floor(seconds / 3600);
seconds -= hours * 3600;
const minutes = Math.floor(seconds / 60);
seconds -= minutes * 60;
if (days > 365) {
const years = days / 365;
return `${years} year${Math.floor(years) === 1 ? "" : "s"}`;
}
if (days > 30) {
const months = Math.floor(days / 30);
const diffDays = days - months * 30;
return `${months} month${Math.floor(months) === 1 ? "" : "s"} ${
diffDays > 0 ? `${diffDays} day${diffDays > 1 ? "s" : ""}` : ""
}`;
}
if (days >= 7 && days <= 30) {
const weeks = Math.floor(days / 7);
return `${Math.floor(weeks)} week${weeks === 1 ? "" : "s"}`;
}
if (days >= 1 && days <= 6) {
return `${days} day${days > 1 ? "s" : ""}`;
}
return `${hours >= 1 ? `${hours} hour${hours > 1 ? "s" : ""}` : ""} ${
minutes >= 1 && hours === 0
? `${minutes} minute${minutes > 1 ? "s" : ""}`
: ""
} ${
seconds >= 1 && minutes === 0 && hours === 0
? `${seconds} second${seconds > 1 ? "s" : ""}`
: ""
}`;
};
export const getTimeFromTimestamp = (
timestamp: string,
fullDate: boolean = false
) => {
const dateObject = new Date(parseInt(timestamp) * 1000);
if (fullDate) {
return `${dateObject.getFullYear()}-${String(
dateObject.getMonth() + 1
).padStart(2, "0")}-${String(dateObject.getDay()).padStart(
2,
"0"
)} ${dateObject.getHours()}:${String(dateObject.getMinutes()).padStart(
2,
"0"
)}:${String(dateObject.getSeconds()).padStart(2, "0")}`;
}
return `${dateObject.getHours()}:${String(dateObject.getMinutes()).padStart(
2,
"0"
)}`;
};
export const calculateBytes = (
x: string,
showDecimals = false,
roundFloor = true
) => {
const bytes = parseInt(x, 10);
if (bytes === 0) {
return { total: 0, unit: k8sCalcUnits[0] };
}
// Gi : GiB
const k = 1024;
// Get unit for measure
const i = Math.floor(Math.log(bytes) / Math.log(k));
const fractionDigits = showDecimals ? 0 : 1;
const bytesUnit = bytes / Math.pow(k, i);
const roundedUnit = roundFloor ? Math.floor(bytesUnit) : bytesUnit;
// Get Unit parsed
const unitParsed = parseFloat(roundedUnit.toFixed(fractionDigits));
const finalUnit = k8sCalcUnits[i];
return { total: unitParsed, unit: finalUnit };
};
export const nsToSeconds = (nanoseconds: number) => {
const conversion = nanoseconds * 0.000000001;
const round = Math.round((conversion + Number.EPSILON) * 10000) / 10000;
return `${round} s`;
};
export const textToRGBColor = (text: string) => {
const splitText = text.split("");
const hashVl = splitText.reduce((acc, currItem) => {
return acc + currItem.charCodeAt(0) + ((acc << 5) - acc);
}, 0);
const hashColored = ((hashVl * 100) & 0x00ffffff).toString(16).toUpperCase();
return `#${hashColored.padStart(6, "0")}`;
};

View File

@@ -22,6 +22,8 @@ import configureStore from "./store";
import * as serviceWorker from "./serviceWorker";
import { ThemeProvider, withStyles } from "@material-ui/core/styles";
import "react-virtualized/styles.css";
import "react-grid-layout/css/styles.css";
import "react-resizable/css/styles.css";
import "./index.css";
import theme from "./theme/main";

View File

@@ -103,9 +103,10 @@ const Account = ({ classes }: IServiceAccountsProps) => {
newServiceAccount,
setNewServiceAccount,
] = useState<NewServiceAccount | null>(null);
const [changePasswordModalOpen, setChangePasswordModalOpen] = useState<
boolean
>(false);
const [
changePasswordModalOpen,
setChangePasswordModalOpen,
] = useState<boolean>(false);
useEffect(() => {
fetchRecords();

View File

@@ -200,7 +200,24 @@ const ListObjects = ({
.invoke("GET", `/api/v1/buckets/${bucketName}/objects${extraPath}`)
.then((res: BucketObjectsList) => {
setSelectedBucket(bucketName);
setRecords(res.objects || []);
const records: BucketObject[] = res.objects || [];
const folders: BucketObject[] = [];
const files: BucketObject[] = [];
records.forEach((record) => {
// this is a folder
if (record.name.endsWith("/")) {
folders.push(record);
} else {
// this is a file
files.push(record);
}
});
const recordsInElement = [...folders, ...files];
setRecords(recordsInElement);
// In case no objects were retrieved, We check if item is a file
if (!res.objects && extraPath !== "") {
verifyIfIsFile();

View File

@@ -216,9 +216,10 @@ const ViewBucket = ({ classes, match }: IViewBucketProps) => {
);
const [curTab, setCurTab] = useState<number>(0);
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
const [enableEncryptionScreenOpen, setEnableEncryptionScreenOpen] = useState<
boolean
>(false);
const [
enableEncryptionScreenOpen,
setEnableEncryptionScreenOpen,
] = useState<boolean>(false);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedEvent, setSelectedEvent] = useState<BucketEvent | null>(null);
const [bucketSize, setBucketSize] = useState<string>("0");

View File

@@ -0,0 +1,92 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import MomentUtils from "@date-io/moment";
import { DateTimePicker, MuiPickersUtilsProvider } from "@material-ui/pickers";
import InputAdornment from "@material-ui/core/InputAdornment";
import ScheduleIcon from "@material-ui/icons/Schedule";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
interface IDateTimePicker {
value: any;
onChange: (value: any) => any;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
dateSelectorOverride: {
height: 40,
border: "#EAEDEE 1px solid",
marginLeft: 15,
backgroundColor: "#fff",
padding: "0 16px",
borderRadius: 5,
"&.MuiInput-underline:hover:not(.Mui-disabled):before": {
borderBottom: 0,
},
"&:hover": {
borderColor: "#000",
"&:before, &:after": {
borderColor: "transparent",
borderBottom: 0,
},
},
"&:before, &:after": {
borderColor: "transparent",
borderBottom: 0,
},
"& input": {
fontSize: 12,
fontWeight: 600,
color: "#393939",
},
},
parentDateOverride: {
flexGrow: 1,
},
});
const DateTimePickerWrapper = ({
value,
onChange,
classes,
}: IDateTimePicker) => {
return (
<MuiPickersUtilsProvider utils={MomentUtils}>
<DateTimePicker
value={value}
onChange={onChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<ScheduleIcon />
</InputAdornment>
),
className: classes.dateSelectorOverride,
}}
label=""
ampm={false}
variant={"inline"}
className={classes.parentDateOverride}
format="MMMM Do YYYY, h:mm a"
/>
</MuiPickersUtilsProvider>
);
};
export default withStyles(styles)(DateTimePickerWrapper);

View File

@@ -0,0 +1,106 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, { Fragment } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import { searchField } from "../common/styleLibrary";
interface IFilterInputWrapper {
classes: any;
value: string;
onChange: (txtVar: string) => any;
label: string;
placeholder?: string;
id: string;
name: string;
}
const styles = (theme: Theme) =>
createStyles({
searchField: {
...searchField.searchField,
height: 30,
padding: 0,
"& input": {
padding: "0 12px",
height: 28,
fontSize: 12,
fontWeight: 600,
color: "#393939",
},
"&.isDisabled": {
"&:hover": {
borderColor: "#EAEDEE",
},
},
"& input.Mui-disabled": {
backgroundColor: "#EAEAEA",
},
},
labelStyle: {
color: "#393939",
fontSize: 12,
marginBottom: 4,
},
buttonKit: {
display: "flex",
alignItems: "center",
},
toggleButton: {
marginRight: 10,
},
fieldContainer: {
flexGrow: 1,
margin: "0 15px",
},
});
const FilterInputWrapper = ({
classes,
label,
onChange,
value,
placeholder = "",
id,
name,
}: IFilterInputWrapper) => {
return (
<Fragment>
<div className={classes.fieldContainer}>
<div className={classes.labelStyle}>{label}</div>
<div className={classes.buttonKit}>
<TextField
placeholder={placeholder}
id={id}
name={name}
label=""
onChange={(val) => {
onChange(val.target.value);
}}
InputProps={{
disableUnderline: true,
}}
className={classes.searchField}
value={value}
/>
</div>
</div>
</Fragment>
);
};
export default withStyles(styles)(FilterInputWrapper);

View File

@@ -34,6 +34,7 @@ interface IFormSwitch {
indicatorLabels?: string[];
checked: boolean;
switchOnly?: boolean;
containerClass?: string;
}
const styles = (theme: Theme) =>
@@ -177,11 +178,12 @@ const FormSwitchWrapper = ({
switchOnly = false,
tooltip = "",
indicatorLabels = [],
containerClass = "",
classes,
}: IFormSwitch) => {
const switchComponent = (
<React.Fragment>
<div className={classes.switchContainer}>
<div className={`${classes.switchContainer} ${containerClass}`}>
<StyledSwitch
checked={checked}
onChange={onChange}

View File

@@ -135,9 +135,22 @@ export const containerForHeader = (bottomSpacing: any) => ({
});
export const actionsTray = {
label: {
color: "#393939",
fontWeight: 600,
fontSize: 13,
alignSelf: "center" as const,
whiteSpace: "nowrap" as const,
"&:not(:first-of-type)": {
marginLeft: 10,
},
},
timeContainers: {
height: 40,
},
actionsTray: {
display: "flex",
justifyContent: "space-between",
display: "flex" as const,
justifyContent: "space-between" as const,
"& button": {
flexGrow: 0,
marginLeft: 15,
@@ -292,3 +305,105 @@ export const settingsCommon = {
margin: "20px 38px 0",
},
};
export const logsCommon = {
logsSubContainer: {
height: "calc(100vh - 230px)",
padding: "15px 33px",
},
};
export const widgetCommon = {
singleValueContainer: {
position: "relative" as const,
flexGrow: 1,
width: "100%",
height: "100%",
border: "#EAEAEA 1px solid",
borderRadius: 5,
backgroundColor: "#fff",
},
titleContainer: {
color: "#393939",
fontWeight: 600,
height: 15,
textAlign: "center" as const,
fontSize: 10,
},
contentContainer: {
flexGrow: 2,
justifyContent: "center" as const,
alignItems: "center" as const,
display: "flex" as const,
position: "absolute" as const,
width: "100%",
height: "calc(100% - 15px)",
},
contentContainerWithLabel: {
height: "calc(100% - 25px)",
},
legendBlock: {
position: "absolute" as const,
bottom: 5,
display: "flex" as const,
width: "100%",
height: 15,
flexWrap: "wrap" as const,
overflowY: "auto" as const,
},
singleLegendContainer: {
display: "flex",
alignItems: "center",
padding: "0 10px",
maxWidth: "100%",
},
colorContainer: {
width: 8,
height: 8,
minWidth: 8,
borderRadius: "100%",
marginRight: 5,
},
legendLabel: {
fontSize: "80%",
color: "#393939",
whiteSpace: "nowrap" as const,
overflow: "hidden" as const,
textOverflow: "ellipsis" as const,
},
};
export const tooltipCommon = {
customTooltip: {
backgroundColor: "rgba(255, 255, 255, 0.90)",
border: "#eaeaea 1px solid",
borderRadius: 3,
padding: "5px 10px",
maxHeight: 300,
overflowY: "auto" as const,
},
labelContainer: {
display: "flex" as const,
alignItems: "center" as const,
},
labelColor: {
width: 6,
height: 6,
display: "block" as const,
borderRadius: "100%",
marginRight: 5,
},
itemValue: {
fontSize: "75%",
color: "#393939",
},
valueContainer: {
fontWeight: 600,
},
timeStampTitle: {
fontSize: "80%",
color: "#9c9c9c",
textAlign: "center" as const,
marginBottom: 6,
},
};

View File

@@ -13,7 +13,7 @@
//
// 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, Fragment } from "react";
import get from "lodash/get";
import isString from "lodash/isString";
import {
@@ -22,15 +22,21 @@ import {
Grid,
Checkbox,
Typography,
IconButton,
Popover,
} from "@material-ui/core";
import { Table, Column, AutoSizer } from "react-virtualized";
import { Table, Column, AutoSizer, InfiniteLoader } from "react-virtualized";
import { createStyles, withStyles } from "@material-ui/core/styles";
import ViewColumnIcon from "@material-ui/icons/ViewColumn";
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
import ArrowDropUpIcon from "@material-ui/icons/ArrowDropUp";
import TableActionButton from "./TableActionButton";
import history from "../../../../history";
import {
checkboxIcons,
radioIcons,
} from "../FormComponents/common/styleLibrary";
import CheckboxWrapper from "../FormComponents/CheckboxWrapper/CheckboxWrapper";
//Interfaces for table Items
@@ -51,6 +57,21 @@ interface IColumns {
width?: number;
headerTextAlign?: string;
contentTextAlign?: string;
enableSort?: boolean;
}
interface IInfiniteScrollConfig {
loadMoreRecords: (indexElements: {
startIndex: number;
stopIndex: number;
}) => Promise<any>;
recordsCount: number;
}
interface ISortConfig {
triggerSort: (val: any) => any;
currentSort: string;
currentDirection: "ASC" | "DESC" | undefined;
}
interface TableWrapperProps {
@@ -67,6 +88,13 @@ interface TableWrapperProps {
customEmptyMessage?: string;
customPaperHeight?: string;
noBackground?: boolean;
columnsSelector?: boolean;
textSelectable?: boolean;
columnsShown?: string[];
onColumnChange?: (column: string, state: boolean) => any;
autoScrollToBottom?: boolean;
infiniteScrollConfig?: IInfiniteScrollConfig;
sortConfig?: ISortConfig;
}
const borderColor = "#9c9c9c80";
@@ -98,7 +126,7 @@ const styles = () =>
borderRadius: 3,
minHeight: 200,
overflowY: "scroll",
position: "relative",
"&::-webkit-scrollbar": {
width: 3,
height: 3,
@@ -169,6 +197,26 @@ const styles = () =>
paddingTop: "100px",
paddingBottom: "100px",
},
overlayColumnSelection: {
position: "absolute",
right: 0,
top: 0,
},
popoverContainer: {
position: "relative",
},
popoverContent: {
maxHeight: 250,
overflowY: "auto",
padding: "0 10px 10px",
},
shownColumnsLabel: {
color: "#9c9c9c",
fontSize: 12,
padding: 10,
borderBottom: "#eaeaea 1px solid",
width: "100%",
},
"@global": {
".rowLine": {
borderBottom: `1px solid ${borderColor}`,
@@ -186,6 +234,9 @@ const styles = () =>
"&.canClick": {
cursor: "pointer",
},
"&.canSelectText": {
userSelect: "text",
},
},
"& .selected": {
color: "#081C42",
@@ -197,6 +248,9 @@ const styles = () =>
fontWeight: 700,
fontSize: 14,
fontStyle: "initial",
display: "flex",
alignItems: "center",
outline: "none",
},
".ReactVirtualized__Table__headerRow": {
fontWeight: 700,
@@ -236,9 +290,9 @@ const subRenderFunction = (
: renderConst; // If render function is set, we send the value to the function.
return (
<React.Fragment>
<Fragment>
<span className={isSelected ? "selected" : ""}>{renderElement}</span>
</React.Fragment>
</Fragment>
);
};
@@ -248,8 +302,18 @@ const calculateColumnRest = (
containerWidth: number,
actionsWidth: number,
hasSelect: boolean,
hasActions: boolean
hasActions: boolean,
columnsSelector: boolean,
columnsShown: string[]
) => {
let colsItems = [...columns];
if (columnsSelector) {
colsItems = columns.filter((column) =>
columnsShown.includes(column.elementKey)
);
}
let initialValue = containerWidth;
if (hasSelect) {
@@ -260,11 +324,11 @@ const calculateColumnRest = (
initialValue -= actionsWidth;
}
let freeSpacing = columns.reduce((total, currValue) => {
let freeSpacing = colsItems.reduce((total, currValue) => {
return currValue.width ? total - currValue.width : total;
}, initialValue);
return freeSpacing / columns.filter((el) => !el.width).length;
return freeSpacing / colsItems.filter((el) => !el.width).length;
};
// Function that renders Columns in table
@@ -275,16 +339,28 @@ const generateColumnsMap = (
hasSelect: boolean,
hasActions: boolean,
selectedItems: string[],
idField: string
idField: string,
columnsSelector: boolean,
columnsShown: string[],
sortColumn: string,
sortDirection: "ASC" | "DESC" | undefined
) => {
const commonRestWidth = calculateColumnRest(
columns,
containerWidth,
actionsWidth,
hasSelect,
hasActions
hasActions,
columnsSelector,
columnsShown
);
return columns.map((column: IColumns, index: number) => {
if (columnsSelector && !columnsShown.includes(column.elementKey)) {
return null;
}
const disableSort = column.enableSort ? !column.enableSort : true;
return (
<Column
key={`col-tb-${index.toString()}`}
@@ -292,7 +368,20 @@ const generateColumnsMap = (
headerClassName={`titleHeader ${
column.headerTextAlign ? `text-${column.headerTextAlign}` : ""
}`}
headerRenderer={() => <React.Fragment>{column.label}</React.Fragment>}
headerRenderer={() => (
<Fragment>
{sortColumn === column.elementKey && (
<Fragment>
{sortDirection === "ASC" ? (
<ArrowDropUpIcon />
) : (
<ArrowDropDownIcon />
)}
</Fragment>
)}
{column.label}
</Fragment>
)}
className={
column.contentTextAlign ? `text-${column.contentTextAlign}` : ""
}
@@ -305,6 +394,8 @@ const generateColumnsMap = (
return subRenderFunction(rowData, column, isSelected);
}}
width={column.width || commonRestWidth}
disableSort={disableSort}
defaultSortDirection={"ASC"}
/>
);
});
@@ -368,7 +459,17 @@ const TableWrapper = ({
customEmptyMessage = "",
customPaperHeight = "",
noBackground = false,
columnsSelector = false,
textSelectable = false,
columnsShown = [],
onColumnChange = (column: string, state: boolean) => {},
infiniteScrollConfig,
sortConfig,
autoScrollToBottom = false,
}: TableWrapperProps) => {
const [columnSelectorOpen, setColumnSelectorOpen] = useState<boolean>(false);
const [anchorEl, setAnchorEl] = React.useState<any>(null);
const findView = itemActions
? itemActions.find((el) => el.type === "view")
: null;
@@ -387,6 +488,64 @@ const TableWrapper = ({
}
};
const openColumnsSelector = (event: { currentTarget: any }) => {
setColumnSelectorOpen(!columnSelectorOpen);
setAnchorEl(event.currentTarget);
};
const closeColumnSelector = () => {
setColumnSelectorOpen(false);
setAnchorEl(null);
};
const columnsSelection = (columns: IColumns[]) => {
return (
<Fragment>
<IconButton
aria-describedby={"columnsSelector"}
color="primary"
onClick={openColumnsSelector}
>
<ViewColumnIcon fontSize="inherit" />
</IconButton>
<Popover
anchorEl={anchorEl}
id={"columnsSelector"}
open={columnSelectorOpen}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
onClose={closeColumnSelector}
className={classes.popoverContainer}
>
<div className={classes.shownColumnsLabel}>Shown Columns</div>
<div className={classes.popoverContent}>
{columns.map((column: IColumns) => {
return (
<CheckboxWrapper
key={`tableColumns-${column.label}`}
label={column.label}
checked={columnsShown.includes(column.elementKey)}
onChange={(e) => {
onColumnChange(column.elementKey, e.target.checked);
}}
id={`chbox-${column.label}`}
name={`chbox-${column.label}`}
value={column.label}
/>
);
})}
</div>
</Popover>
</Fragment>
);
};
return (
<Grid item xs={12}>
<Paper
@@ -408,7 +567,26 @@ const TableWrapper = ({
</Grid>
</Grid>
)}
{columnsSelector && !isLoading && records.length > 0 && (
<div className={classes.overlayColumnSelection}>
{columnsSelection(columns)}
</div>
)}
{records && !isLoading && records.length > 0 ? (
<InfiniteLoader
isRowLoaded={({ index }) => !!records[index]}
loadMoreRows={
infiniteScrollConfig
? infiniteScrollConfig.loadMoreRecords
: () => new Promise(() => true)
}
rowCount={
infiniteScrollConfig
? infiniteScrollConfig.recordsCount
: records.length
}
>
{({ onRowsRendered, registerChild }) => (
<AutoSizer>
{({ width, height }: any) => {
const optionsWidth = calculateOptionsSize(
@@ -426,17 +604,17 @@ const TableWrapper = ({
);
return (
<Table
ref="Table"
ref={registerChild}
disableHeader={false}
headerClassName={"headerItem"}
headerHeight={40}
height={height}
noRowsRenderer={() => (
<React.Fragment>
<Fragment>
{customEmptyMessage !== ""
? customEmptyMessage
: `There are no ${entityName} yet.`}
</React.Fragment>
</Fragment>
)}
overscanRowCount={10}
rowHeight={40}
@@ -446,13 +624,22 @@ const TableWrapper = ({
onRowClick={({ rowData }) => {
clickAction(rowData);
}}
rowClassName={`rowLine ${findView ? "canClick" : ""}`}
rowClassName={`rowLine ${findView ? "canClick" : ""} ${
!findView && textSelectable ? "canSelectText" : ""
}`}
onRowsRendered={onRowsRendered}
sort={sortConfig ? sortConfig.triggerSort : undefined}
sortBy={sortConfig ? sortConfig.currentSort : undefined}
sortDirection={
sortConfig ? sortConfig.currentDirection : undefined
}
scrollToIndex={
autoScrollToBottom ? records.length - 1 : -1
}
>
{hasSelect && (
<Column
headerRenderer={() => (
<React.Fragment>Select</React.Fragment>
)}
headerRenderer={() => <Fragment>Select</Fragment>}
dataKey={idField}
width={selectWidth}
cellRenderer={({ rowData }) => {
@@ -506,13 +693,15 @@ const TableWrapper = ({
hasSelect,
hasOptions,
selectedItems || [],
idField
idField,
columnsSelector,
columnsShown,
sortConfig ? sortConfig.currentSort : "",
sortConfig ? sortConfig.currentDirection : undefined
)}
{hasOptions && (
<Column
headerRenderer={() => (
<React.Fragment>Options</React.Fragment>
)}
headerRenderer={() => <Fragment>Options</Fragment>}
dataKey={idField}
width={optionsWidth}
headerClassName="optionsAlignment"
@@ -536,8 +725,10 @@ const TableWrapper = ({
);
}}
</AutoSizer>
)}
</InfiniteLoader>
) : (
<React.Fragment>
<Fragment>
{!isLoading && (
<div>
{customEmptyMessage !== ""
@@ -545,7 +736,7 @@ const TableWrapper = ({
: `There are no ${entityName} yet.`}
</div>
)}
</React.Fragment>
</Fragment>
)}
</Paper>
</Grid>

View File

@@ -21,6 +21,7 @@ import { Button, LinearProgress } from "@material-ui/core";
import CssBaseline from "@material-ui/core/CssBaseline";
import Drawer from "@material-ui/core/Drawer";
import Container from "@material-ui/core/Container";
import Snackbar from "@material-ui/core/Snackbar";
import history from "../../history";
import { Redirect, Route, Router, Switch } from "react-router-dom";
import { connect } from "react-redux";
@@ -48,10 +49,9 @@ import ObjectBrowser from "./ObjectBrowser/ObjectBrowser";
import ObjectRouting from "./Buckets/ListBuckets/Objects/ListObjects/ObjectRouting";
import License from "./License/License";
import Trace from "./Trace/Trace";
import Logs from "./Logs/Logs";
import LogsMain from "./Logs/LogsMain";
import Heal from "./Heal/Heal";
import Watch from "./Watch/Watch";
import Snackbar from "@material-ui/core/Snackbar";
const drawerWidth = 245;
@@ -285,7 +285,7 @@ const Console = ({
path: "/trace",
},
{
component: Logs,
component: LogsMain,
path: "/logs",
},
{

View File

@@ -0,0 +1,191 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, { Fragment } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import clsx from "clsx";
import Grid from "@material-ui/core/Grid";
import Paper from "@material-ui/core/Paper";
import Typography from "@material-ui/core/Typography";
import { Usage } from "../types";
import { niceBytes } from "../../../../common/utils";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import AllBucketsIcon from "../../../../icons/AllBucketsIcon";
import UsageIcon from "../../../../icons/UsageIcon";
import EgressIcon from "../../../../icons/EgressIcon";
import ErrorBlock from "../../../shared/ErrorBlock";
const styles = (theme: Theme) =>
createStyles({
...containerForHeader(theme.spacing(4)),
paper: {
padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column",
border: "#eaedee 1px solid",
borderRadius: 5,
boxShadow: "none",
},
fixedHeight: {
height: 165,
minWidth: 247,
marginRight: 20,
padding: "25px 28px",
"& svg": {
maxHeight: 18,
},
},
consumptionValue: {
color: "#000000",
fontSize: "60px",
fontWeight: "bold",
},
icon: {
marginRight: 10,
color: "#777777",
},
notationContainer: {
display: "flex",
},
dashboardBG: {
width: 390,
height: 255,
zIndex: 500,
position: "absolute",
backgroundSize: "fill",
backgroundImage: "url(/images/BG_IllustrationDarker.svg)",
backgroundPosition: "right bottom",
right: 0,
bottom: 0,
backgroundRepeat: "no-repeat",
},
dashboardContainer: {
zIndex: 600,
position: "absolute",
},
elementTitle: {
fontWeight: 500,
color: "#777777",
fontSize: 14,
marginTop: -9,
},
smallUnit: {
fontSize: 20,
},
});
interface IDashboardProps {
classes: any;
usage: Usage | null;
error: string;
}
const BasicDashboard = ({ classes, usage, error }: IDashboardProps) => {
const fixedHeightPaper = clsx(classes.paper, classes.fixedHeight);
const prettyUsage = (usage: string | undefined) => {
if (usage === undefined) {
return "0";
}
const niceBytesUsage = niceBytes(usage).split(" ");
if (niceBytesUsage.length !== 2) {
return niceBytesUsage.join(" ");
}
return (
<Fragment>
{niceBytesUsage[0]}
<span className={classes.smallUnit}>{niceBytesUsage[1]}</span>
</Fragment>
);
};
const prettyNumber = (usage: number | undefined) => {
if (usage === undefined) {
return 0;
}
return usage.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
return (
<Fragment>
<div className={classes.dashboardBG} />
<Grid container className={classes.dashboardContainer}>
<Grid container spacing={3} className={classes.container}>
{error !== "" && (
<Grid container>
<ErrorBlock errorMessage={error} />
</Grid>
)}
<Grid item className={classes.notationContainer}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<AllBucketsIcon />
</Grid>
<Grid item>
<Typography className={classes.elementTitle}>
All buckets
</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyNumber(usage.buckets) : 0}
</Typography>
</Paper>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<UsageIcon />
</Grid>
<Grid item>
<Typography className={classes.elementTitle}>
Usage
</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyUsage(usage.usage + "") : 0}
</Typography>
</Paper>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<EgressIcon />
</Grid>
<Grid item>
<Typography className={classes.elementTitle}>
{" "}
Total Objects
</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyNumber(usage.objects) : 0}
</Typography>
</Paper>
</Grid>
</Grid>
</Grid>
</Fragment>
);
};
export default withStyles(styles)(BasicDashboard);

View File

@@ -14,139 +14,30 @@
// 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import clsx from "clsx";
import Grid from "@material-ui/core/Grid";
import Paper from "@material-ui/core/Paper";
import Typography from "@material-ui/core/Typography";
import { Usage } from "./types";
import api from "../../../common/api";
import { niceBytes } from "../../../common/utils";
import { LinearProgress } from "@material-ui/core";
import React, { useEffect, useState, Fragment } from "react";
import get from "lodash/get";
import PrDashboard from "./Prometheus/PrDashboard";
import PageHeader from "../Common/PageHeader/PageHeader";
import Grid from "@material-ui/core/Grid";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import AllBucketsIcon from "../../../icons/AllBucketsIcon";
import UsageIcon from "../../../icons/UsageIcon";
import EgressIcon from "../../../icons/EgressIcon";
import ErrorBlock from "../../shared/ErrorBlock";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import BasicDashboard from "./BasicDashboard/BasicDashboard";
import { LinearProgress } from "@material-ui/core";
import api from "../../../common/api";
import { Usage } from "./types";
const styles = (theme: Theme) =>
createStyles({
root: {
display: "flex",
},
toolbar: {
paddingRight: 24, // keep right padding when drawer closed
},
toolbarIcon: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: "0 8px",
...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,
}),
},
menuButton: {
marginRight: 36,
},
menuButtonHidden: {
display: "none",
},
title: {
flexGrow: 1,
},
drawerPaperClose: {
overflowX: "hidden",
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
width: theme.spacing(7),
[theme.breakpoints.up("sm")]: {
width: theme.spacing(9),
},
},
appBarSpacer: theme.mixins.toolbar,
content: {
flexGrow: 1,
height: "100vh",
overflow: "auto",
},
...containerForHeader(theme.spacing(4)),
paper: {
padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column",
border: "#eaedee 1px solid",
borderRadius: 5,
boxShadow: "none",
},
fixedHeight: {
height: 165,
minWidth: 247,
marginRight: 20,
padding: "25px 28px",
"& svg": {
maxHeight: 18,
},
},
consumptionValue: {
color: "#000000",
fontSize: "60px",
fontWeight: "bold",
},
icon: {
marginRight: 10,
color: "#777777",
},
notationContainer: {
display: "flex",
},
dashboardBG: {
width: 390,
height: 255,
zIndex: 500,
position: "absolute",
backgroundSize: "fill",
backgroundImage: "url(/images/BG_IllustrationDarker.svg)",
backgroundPosition: "right bottom",
right: 0,
bottom: 0,
backgroundRepeat: "no-repeat",
},
dashboardContainer: {
zIndex: 600,
position: "absolute",
},
elementTitle: {
fontWeight: 500,
color: "#777777",
fontSize: 14,
marginTop: -9,
},
smallUnit: {
fontSize: 20,
},
});
interface IDashboardProps {
interface IDashboardSimple {
classes: any;
}
const Dashboard = ({ classes }: IDashboardProps) => {
const fixedHeightPaper = clsx(classes.paper, classes.fixedHeight);
const [usage, setUsage] = useState<Usage | null>(null);
const styles = (theme: Theme) =>
createStyles({
...containerForHeader(theme.spacing(4)),
});
const Dashboard = ({ classes }: IDashboardSimple) => {
const [loading, setLoading] = useState<boolean>(true);
const [basicResult, setBasicResult] = useState<Usage | null>(null);
const [error, setError] = useState<string>("");
useEffect(() => {
@@ -159,7 +50,7 @@ const Dashboard = ({ classes }: IDashboardProps) => {
api
.invoke("GET", `/api/v1/admin/info`)
.then((res: Usage) => {
setUsage(res);
setBasicResult(res);
setError("");
setLoading(false);
})
@@ -168,103 +59,28 @@ const Dashboard = ({ classes }: IDashboardProps) => {
setLoading(false);
});
};
const prettyUsage = (usage: string | undefined) => {
if (usage === undefined) {
return "0";
}
const niceBytesUsage = niceBytes(usage).split(" ");
if (niceBytesUsage.length !== 2) {
return niceBytesUsage.join(" ");
}
const widgets = get(basicResult, "widgets", null);
return (
<React.Fragment>
{niceBytesUsage[0]}
<span className={classes.smallUnit}>{niceBytesUsage[1]}</span>
</React.Fragment>
);
};
const prettyNumber = (usage: number | undefined) => {
if (usage === undefined) {
return 0;
}
return usage.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
return (
<React.Fragment>
<Fragment>
<PageHeader label="Dashboard" />
<div className={classes.dashboardBG} />
<Grid container className={classes.dashboardContainer}>
<Grid container spacing={3} className={classes.container}>
{error !== "" && (
<Grid container>
<ErrorBlock errorMessage={error} />
</Grid>
)}
{loading ? (
<Grid item xs={12} md={12} lg={12}>
<Grid item xs={12} className={classes.container}>
<LinearProgress />
</Grid>
) : (
<React.Fragment>
<Grid item className={classes.notationContainer}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<AllBucketsIcon />
</Grid>
<Grid item>
<Typography className={classes.elementTitle}>
All buckets
</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyNumber(usage.buckets) : 0}
</Typography>
</Paper>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<UsageIcon />
</Grid>
<Grid item>
<Typography className={classes.elementTitle}>
Usage
</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyUsage(usage.usage + "") : 0}
</Typography>
</Paper>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<EgressIcon />
</Grid>
<Grid item>
<Typography className={classes.elementTitle}>
{" "}
Total Objects
</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{usage ? prettyNumber(usage.objects) : 0}
</Typography>
</Paper>
</Grid>
</React.Fragment>
<Fragment>
{widgets !== null ? (
<PrDashboard />
) : (
<BasicDashboard usage={basicResult} error={error} />
)}
</Fragment>
)}
</Grid>
</Grid>
</React.Fragment>
</Fragment>
);
};

View File

@@ -0,0 +1,246 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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,
useMemo,
useState,
Fragment,
useCallback,
} from "react";
import ReactGridLayout from "react-grid-layout";
import Grid from "@material-ui/core/Grid";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
actionsTray,
containerForHeader,
} from "../../Common/FormComponents/common/styleLibrary";
import SingleValueWidget from "./Widgets/SingleValueWidget";
import { AutoSizer } from "react-virtualized";
import LinearGraphWidget from "./Widgets/LinearGraphWidget";
import {
IBarChartConfiguration,
IDataSRep,
ILinearGraphConfiguration,
IPieChartConfiguration,
} from "./Widgets/types";
import BarChartWidget from "./Widgets/BarChartWidget";
import PieChartWidget from "./Widgets/PieChartWidget";
import SingleRepWidget from "./Widgets/SingleRepWidget";
import DateTimePickerWrapper from "../../Common/FormComponents/DateTimePickerWrapper/DateTimePickerWrapper";
import { IDashboardPanel, widgetType } from "./types";
import api from "../../../../common/api";
import {
getDashboardDistribution,
getWidgetsWithValue,
panelsConfiguration,
saveDashboardDistribution,
} from "./utils";
import { Button } from "@material-ui/core";
import ErrorBlock from "../../../shared/ErrorBlock";
interface IPrDashboard {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
widgetsContainer: {
height: "calc(100vh - 250px)",
},
...actionsTray,
...containerForHeader(theme.spacing(4)),
});
const PrDashboard = ({ classes }: IPrDashboard) => {
const [timeStart, setTimeStart] = useState<any>(null);
const [timeEnd, setTimeEnd] = useState<any>(null);
const [loading, setLoading] = useState<boolean>(true);
const [panelInformation, setPanelInformation] = useState<IDashboardPanel[]>(
panelsConfiguration
);
const [error, setError] = useState<string>("");
const minHeight = 600;
const panels = useMemo(() => {
const componentToUse = (value: IDashboardPanel) => {
switch (value.type) {
case widgetType.singleValue:
return (
<SingleValueWidget
title={value.title}
data={value.data as string}
/>
);
case widgetType.pieChart:
return (
<PieChartWidget
title={value.title}
dataInner={value.data as object[]}
dataOuter={(value.dataOuter as object[]) || null}
pieChartConfiguration={
value.widgetConfiguration as IPieChartConfiguration
}
middleLabel={value.innerLabel}
/>
);
case widgetType.linearGraph:
return (
<LinearGraphWidget
title={value.title}
data={value.data as object[]}
linearConfiguration={
value.widgetConfiguration as ILinearGraphConfiguration[]
}
hideYAxis={value.disableYAxis}
xAxisFormatter={value.xAxisFormatter}
yAxisFormatter={value.yAxisFormatter}
/>
);
case widgetType.barChart:
return (
<BarChartWidget
title={value.title}
data={value.data as object[]}
barChartConfiguration={
value.widgetConfiguration as IBarChartConfiguration[]
}
/>
);
case widgetType.singleRep:
const fillColor = value.fillColor ? value.fillColor : value.color;
return (
<SingleRepWidget
title={value.title}
data={value.data as IDataSRep[]}
label={value.innerLabel as string}
color={value.color as string}
fillColor={fillColor as string}
/>
);
default:
return null;
}
};
return panelInformation.map((val, idx) => {
return <div key={val.layoutIdentifier}>{componentToUse(val)}</div>;
});
}, [panelInformation]);
const fetchUsage = useCallback(() => {
let stepCalc = 15;
if (timeStart !== null && timeEnd !== null) {
const secondsInPeriod = timeEnd.unix() - timeStart.unix();
const periods = secondsInPeriod / 60;
stepCalc = periods < 1 ? 15 : periods;
}
api
.invoke(
"GET",
`/api/v1/admin/info?step=${stepCalc}&${
timeStart !== null ? `&start=${timeStart.unix()}` : ""
}${timeStart !== null && timeEnd !== null ? "&" : ""}${
timeEnd !== null ? `end=${timeEnd.unix()}` : ""
}`
)
.then((res: any) => {
if (res.widgets) {
const widgetsWithValue = getWidgetsWithValue(res.widgets);
setPanelInformation(widgetsWithValue);
setError("");
} else {
setError(
"Widget information could not be retrieved at this time. Please try again"
);
}
setLoading(false);
})
.catch((err) => {
setError(err);
setLoading(false);
});
}, [timeStart, timeEnd]);
const triggerLoad = () => {
setLoading(true);
};
useEffect(() => {
if (loading) {
fetchUsage();
}
}, [loading, fetchUsage]);
const dashboardDistr = getDashboardDistribution();
return (
<Grid container className={classes.container}>
<Grid item xs={12}>
{error !== "" && (
<Fragment>
<ErrorBlock errorMessage={error} withBreak={false} />
<br />
</Fragment>
)}
</Grid>
<Grid
item
xs={12}
className={`${classes.actionsTray} ${classes.timeContainers}`}
>
<span className={classes.label}>Start Time</span>
<DateTimePickerWrapper value={timeStart} onChange={setTimeStart} />
<span className={classes.label}>End Time</span>
<DateTimePickerWrapper value={timeEnd} onChange={setTimeEnd} />
<Button
type="button"
variant="contained"
color="primary"
onClick={triggerLoad}
>
Get Information
</Button>
</Grid>
<Grid item xs={12} className={classes.widgetsContainer}>
<AutoSizer>
{({ width, height }: any) => {
const hpanel = height < minHeight ? minHeight : height;
return (
<ReactGridLayout
width={width}
cols={8}
containerPadding={[10, 10]}
onLayoutChange={saveDashboardDistribution}
layout={dashboardDistr}
rowHeight={hpanel / 6}
>
{panels}
</ReactGridLayout>
);
}}
</AutoSizer>
</Grid>
</Grid>
);
};
export default withStyles(styles)(PrDashboard);

View File

@@ -0,0 +1,106 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import {
Bar,
BarChart,
ResponsiveContainer,
XAxis,
Text,
YAxis,
Tooltip,
} from "recharts";
import { IBarChartConfiguration } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary";
import BarChartTooltip from "./tooltips/BarChartTooltip";
interface IBarChartWidget {
classes: any;
title: string;
barChartConfiguration: IBarChartConfiguration[];
data: object[];
}
const styles = (theme: Theme) =>
createStyles({
...widgetCommon,
});
const CustomizedAxisTick = ({ x, y, payload }: any) => {
return (
<g transform={`translate(${x},${y})`}>
<Text
width={50}
fontSize={"63%"}
textAnchor="end"
verticalAnchor="middle"
angle={0}
fill="#333"
>
{payload.value}
</Text>
</g>
);
};
const BarChartWidget = ({
classes,
title,
barChartConfiguration,
data,
}: IBarChartWidget) => {
return (
<div className={classes.singleValueContainer}>
<div className={classes.titleContainer}>{title}</div>
<div className={classes.contentContainer}>
<ResponsiveContainer>
<BarChart data={data} layout={"vertical"} barCategoryGap={1}>
<XAxis type="number" hide />
<YAxis
dataKey="name"
type="category"
interval={0}
tick={<CustomizedAxisTick />}
tickLine={false}
axisLine={false}
width={150}
/>
{barChartConfiguration.map((bar) => (
<Bar
key={`bar-${bar.dataKey}`}
dataKey={bar.dataKey}
fill={bar.color}
background={bar.background}
/>
))}
<Tooltip
cursor={{ fill: "rgba(255, 255, 255, 0.3)" }}
content={
<BarChartTooltip
barChartConfiguration={barChartConfiguration}
/>
}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};
export default withStyles(styles)(BarChartWidget);

View File

@@ -0,0 +1,157 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import {
Area,
AreaChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { ILinearGraphConfiguration } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary";
import LineChartTooltip from "./tooltips/LineChartTooltip";
interface ILinearGraphWidget {
classes: any;
title: string;
linearConfiguration: ILinearGraphConfiguration[];
data: object[];
hideYAxis?: boolean;
yAxisFormatter?: (item: string) => string;
xAxisFormatter?: (item: string) => string;
}
const styles = (theme: Theme) =>
createStyles({
...widgetCommon,
containerElements: {
display: "flex",
flexDirection: "column",
height: "calc(100% - 18px)",
},
chartCont: {
position: "relative",
flexGrow: 1,
minHeight: "65%",
height: 1,
},
legendChart: {
display: "flex",
flexWrap: "wrap",
flex: "0 1 auto",
maxHeight: "35%",
margin: 0,
overflowY: "auto",
position: "relative",
textAlign: "center",
},
});
const LinearGraphWidget = ({
classes,
title,
linearConfiguration,
data,
hideYAxis = false,
yAxisFormatter = (item: string) => item,
xAxisFormatter = (item: string) => item,
}: ILinearGraphWidget) => {
return (
<div className={classes.singleValueContainer}>
<div className={classes.titleContainer}>{title}</div>
<div className={classes.containerElements}>
<div className={classes.chartCont}>
<ResponsiveContainer>
<AreaChart
data={data}
margin={{
top: 5,
right: 20,
left: hideYAxis ? 20 : 5,
bottom: 0,
}}
>
<CartesianGrid
strokeDasharray="3 3"
strokeWidth={1}
strokeOpacity={0.5}
/>
<XAxis
dataKey="name"
tickFormatter={xAxisFormatter}
interval={5}
tick={{ fontSize: "70%" }}
tickCount={10}
/>
<YAxis
domain={[0, (dataMax) => dataMax * 4]}
hide={hideYAxis}
tickFormatter={yAxisFormatter}
tick={{ fontSize: "70%" }}
/>
{linearConfiguration.map((section, index) => {
return (
<Area
key={`area-${section.dataKey}-${index.toString()}`}
type="monotone"
dataKey={section.dataKey}
stroke={section.lineColor}
fill={section.fillColor}
fillOpacity={0.3}
/>
);
})}
<Tooltip
content={
<LineChartTooltip
linearConfiguration={linearConfiguration}
yAxisFormatter={yAxisFormatter}
/>
}
wrapperStyle={{
zIndex: 5000,
}}
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className={classes.legendChart}>
{linearConfiguration.map((section, index) => {
return (
<div
className={classes.singleLegendContainer}
key={`legend-${section.keyLabel}-${index.toString()}`}
>
<div
className={classes.colorContainer}
style={{ backgroundColor: section.lineColor }}
/>
<div className={classes.legendLabel}>{section.keyLabel}</div>
</div>
);
})}
</div>
</div>
</div>
);
};
export default withStyles(styles)(LinearGraphWidget);

View File

@@ -0,0 +1,156 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import get from "lodash/get";
import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts";
import { IPieChartConfiguration } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary";
interface IPieChartWidget {
classes: any;
title: string;
pieChartConfiguration: IPieChartConfiguration;
dataInner: object[];
dataOuter?: object[];
middleLabel?: string;
}
const styles = (theme: Theme) =>
createStyles({
...widgetCommon,
});
const PieChartWidget = ({
classes,
title,
pieChartConfiguration,
dataInner,
dataOuter,
middleLabel = "",
}: IPieChartWidget) => {
const innerColors = get(pieChartConfiguration, "innerChart.colorList", []);
const outerColors = get(pieChartConfiguration, "outerChart.colorList", []);
return (
<div className={classes.singleValueContainer}>
<div className={classes.titleContainer}>{title}</div>
<div className={classes.contentContainer}>
<ResponsiveContainer>
<PieChart margin={{ top: 5, bottom: 5 }}>
{dataOuter && (
<Pie
data={dataOuter}
cx={"50%"}
cy={"50%"}
dataKey="value"
innerRadius={get(
pieChartConfiguration,
"outerChart.innerRadius",
0
)}
outerRadius={get(
pieChartConfiguration,
"outerChart.outerRadius",
"80%"
)}
startAngle={get(
pieChartConfiguration,
"outerChart.startAngle",
0
)}
endAngle={get(
pieChartConfiguration,
"outerChart.endAngle",
360
)}
fill="#201763"
>
{dataOuter.map((entry, index) => (
<Cell
key={`cellOuter-${index}`}
fill={
typeof outerColors[index] == "undefined"
? "#393939"
: outerColors[index]
}
/>
))}
</Pie>
)}
{dataInner && (
<Pie
data={dataInner}
dataKey="value"
cx={"50%"}
cy={"50%"}
innerRadius={get(
pieChartConfiguration,
"innerChart.innerRadius",
0
)}
outerRadius={get(
pieChartConfiguration,
"innerChart.outerRadius",
"80%"
)}
startAngle={get(
pieChartConfiguration,
"innerChart.startAngle",
0
)}
endAngle={get(
pieChartConfiguration,
"innerChart.endAngle",
360
)}
fill="#201763"
>
{dataInner.map((entry, index) => {
return (
<Cell
key={`cell-${index}`}
fill={
typeof innerColors[index] == "undefined"
? "#393939"
: innerColors[index]
}
/>
);
})}
</Pie>
)}
{middleLabel && (
<text
x={"50%"}
y={"50%"}
textAnchor="middle"
dominantBaseline="middle"
fontWeight={600}
fontSize={14}
>
{middleLabel}
</text>
)}
</PieChart>
</ResponsiveContainer>
</div>
</div>
);
};
export default withStyles(styles)(PieChartWidget);

View File

@@ -0,0 +1,77 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Area, AreaChart, ResponsiveContainer, YAxis } from "recharts";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary";
import { IDataSRep } from "./types";
interface ISingleRepWidget {
classes: any;
title: string;
data: IDataSRep[];
color: string;
fillColor: string;
label: string;
}
const styles = (theme: Theme) =>
createStyles({
...widgetCommon,
});
const SingleRepWidget = ({
classes,
title,
data,
color,
fillColor,
label,
}: ISingleRepWidget) => {
return (
<div className={classes.singleValueContainer}>
<div className={classes.titleContainer}>{title}</div>
<div className={classes.contentContainer}>
<ResponsiveContainer>
<AreaChart data={data}>
<YAxis domain={[0, (dataMax) => dataMax * 2]} hide={true} />
<Area
type="monotone"
dataKey={"value"}
stroke={color}
fill={fillColor}
fillOpacity={1}
/>
<text
x={"50%"}
y={"50%"}
textAnchor="middle"
dominantBaseline="middle"
fontWeight={600}
fontSize={18}
fill={color}
>
{label}
</text>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
);
};
export default withStyles(styles)(SingleRepWidget);

View File

@@ -0,0 +1,48 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary";
interface ISingleValueWidget {
title: string;
data: string;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
...widgetCommon,
contentContainer: {
...widgetCommon.contentContainer,
fontWeight: 700,
color: "#072045",
fontSize: 18,
textAlign: "center" as const,
},
});
const SingleValueWidget = ({ title, data, classes }: ISingleValueWidget) => {
return (
<div className={classes.singleValueContainer}>
<div className={classes.titleContainer}>{title}</div>
<div className={classes.contentContainer}>{data}</div>
</div>
);
};
export default withStyles(styles)(SingleValueWidget);

View File

@@ -0,0 +1,63 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { tooltipCommon } from "../../../../Common/FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
...tooltipCommon,
});
const BarChartTooltip = ({
active,
payload,
label,
barChartConfiguration,
classes,
}: any) => {
if (active) {
return (
<div className={classes.customTooltip}>
<div className={classes.timeStampTitle}>{label}</div>
{payload &&
payload.map((pl: any, index: number) => {
return (
<div
className={classes.labelContainer}
key={`pltiem-${index}-${label}`}
>
<div
className={classes.labelColor}
style={{
backgroundColor: barChartConfiguration[index].color,
}}
/>
<div className={classes.itemValue}>
<span className={classes.valueContainer}>{pl.value}</span>
</div>
</div>
);
})}
</div>
);
}
return null;
};
export default withStyles(styles)(BarChartTooltip);

View File

@@ -0,0 +1,70 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { getTimeFromTimestamp } from "../../../../../../common/utils";
import { tooltipCommon } from "../../../../Common/FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
...tooltipCommon,
});
const LineChartTooltip = ({
active,
payload,
label,
linearConfiguration,
yAxisFormatter,
classes,
}: any) => {
if (active) {
return (
<div className={classes.customTooltip}>
<div className={classes.timeStampTitle}>
{getTimeFromTimestamp(label, true)}
</div>
{payload &&
payload.map((pl: any, index: number) => {
return (
<div
className={classes.labelContainer}
key={`lbPl-${index}-${linearConfiguration[index].keyLabel}`}
>
<div
className={classes.labelColor}
style={{
backgroundColor: linearConfiguration[index].lineColor,
}}
/>
<div className={classes.itemValue}>
<span className={classes.valueContainer}>
{linearConfiguration[index].keyLabel}:{" "}
{yAxisFormatter(pl.value)}
</span>
</div>
</div>
);
})}
</div>
);
}
return null;
};
export default withStyles(styles)(LineChartTooltip);

View File

@@ -0,0 +1,45 @@
// 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 ILinearGraphConfiguration {
dataKey: string;
keyLabel: string;
lineColor: string;
fillColor: string;
}
export interface IBarChartConfiguration {
dataKey: string;
color: string;
background?: object;
}
export interface IPieChartConfiguration {
innerChart: ISinglePieConfiguration;
outerChart?: ISinglePieConfiguration;
}
export interface ISinglePieConfiguration {
colorList: string[];
startAngle?: number;
endAngle?: number;
innerRadius?: number | string;
outerRadius?: number | string;
}
export interface IDataSRep {
value: number;
}

View File

@@ -0,0 +1,49 @@
// 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 {
IBarChartConfiguration,
IDataSRep,
ILinearGraphConfiguration,
IPieChartConfiguration,
} from "./Widgets/types";
export enum widgetType {
singleValue = "singleValue",
linearGraph = "linearGraph",
barChart = "barChart",
pieChart = "pieChart",
singleRep = "singleRep",
}
export interface IDashboardPanel {
title: string;
data: string | object[] | IDataSRep[];
dataOuter?: string | object[];
type: widgetType;
layoutIdentifier: string;
widgetConfiguration?:
| ILinearGraphConfiguration[]
| IBarChartConfiguration[]
| IPieChartConfiguration;
color?: string;
fillColor?: string;
innerLabel?: string;
labelDisplayFunction?: (value: string) => any;
disableYAxis?: boolean;
xAxisFormatter?: (item: string) => string;
yAxisFormatter?: (item: string) => string;
}

View File

@@ -0,0 +1,605 @@
// 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 get from "lodash/get";
import { Layout } from "react-grid-layout";
import { IDashboardPanel, widgetType } from "./types";
import {
getTimeFromTimestamp,
niceBytes,
niceDays,
textToRGBColor,
} from "../../../../common/utils";
const dLocalStorageV = "dashboardConfig";
export const defaultWidgetsLayout: Layout[] = [
{ w: 1, h: 2, x: 0, y: 0, i: "panel-0", moved: false, static: false },
{ w: 1, h: 1, x: 1, y: 0, i: "panel-1", moved: false, static: false },
{ w: 1, h: 1, x: 1, y: 1, i: "panel-2", moved: false, static: false },
{ w: 1, h: 2, x: 2, y: 0, i: "panel-3", moved: false, static: false },
{ w: 2, h: 2, x: 3, y: 0, i: "panel-4", moved: false, static: false },
{ w: 3, h: 2, x: 5, y: 0, i: "panel-5", moved: false, static: false },
{ w: 1, h: 1, x: 0, y: 2, i: "panel-6", moved: false, static: false },
{ w: 1, h: 1, x: 0, y: 3, i: "panel-7", moved: false, static: false },
{ w: 1, h: 1, x: 1, y: 2, i: "panel-8", moved: false, static: false },
{ w: 1, h: 1, x: 1, y: 3, i: "panel-9", moved: false, static: false },
{ w: 1, h: 1, x: 2, y: 2, i: "panel-10", moved: false, static: false },
{ w: 1, h: 1, x: 2, y: 3, i: "panel-11", moved: false, static: false },
{ w: 4, h: 2, x: 3, y: 2, i: "panel-12", moved: false, static: false },
{ w: 1, h: 1, x: 7, y: 2, i: "panel-13", moved: false, static: false },
{ w: 1, h: 1, x: 7, y: 3, i: "panel-14", moved: false, static: false },
{ w: 8, h: 2, x: 0, y: 4, i: "panel-15", moved: false, static: false },
{ w: 4, h: 2, x: 0, y: 5, i: "panel-16", moved: false, static: false },
{ w: 4, h: 2, x: 5, y: 5, i: "panel-17", moved: false, static: false },
{ w: 8, h: 2, x: 0, y: 7, i: "panel-18", moved: false, static: false },
{ w: 4, h: 2, x: 0, y: 9, i: "panel-19", moved: false, static: false },
{ w: 4, h: 2, x: 5, y: 9, i: "panel-20", moved: false, static: false },
];
const colorsMain = [
"#6992B7",
"#E2AD17",
"#22B573",
"#F7655E",
"#0071BC",
"#F9E6C5",
"#A6E8C4",
"#F4CECE",
"#ADD5E0",
];
export const panelsConfiguration: IDashboardPanel[] = [
{
title: "Uptime",
data: "N/A",
type: widgetType.singleValue,
layoutIdentifier: "panel-0",
labelDisplayFunction: niceDays,
},
{
title: "Total Online disks",
data: "N/A",
type: widgetType.singleValue,
layoutIdentifier: "panel-1",
},
{
title: "Total Offline disks",
data: "N/A",
type: widgetType.singleValue,
layoutIdentifier: "panel-2",
},
{
title: "Total Data",
data: [],
dataOuter: [{ name: "outer", value: 100 }],
widgetConfiguration: {
outerChart: {
colorList: ["#9c9c9c"],
innerRadius: 51,
outerRadius: 54,
startAngle: -15,
endAngle: 195,
},
innerChart: {
colorList: colorsMain,
innerRadius: 35,
outerRadius: 50,
startAngle: -15,
endAngle: 195,
},
},
type: widgetType.pieChart,
layoutIdentifier: "panel-3",
innerLabel: "N/A",
labelDisplayFunction: niceBytes,
},
{
title: "Data Growth",
data: [],
widgetConfiguration: [
{
dataKey: "",
keyLabel: "",
lineColor: "#000",
fillColor: "#000",
},
],
type: widgetType.linearGraph,
layoutIdentifier: "panel-4",
yAxisFormatter: niceBytes,
xAxisFormatter: getTimeFromTimestamp,
},
{
title: "Object size distribution",
data: [],
widgetConfiguration: [
{
dataKey: "a",
color: colorsMain[0],
background: {
fill: "rgba(0,0,0,0.1)",
},
},
],
type: widgetType.barChart,
layoutIdentifier: "panel-5",
},
{
title: "Total Online Servers",
data: "N/A",
type: widgetType.singleValue,
layoutIdentifier: "panel-6",
},
{
title: "Total Offline Servers",
data: "N/A",
type: widgetType.singleValue,
layoutIdentifier: "panel-7",
},
{
title: "Total S3 Traffic Inbound",
data: [],
innerLabel: "N/A",
type: widgetType.singleRep,
layoutIdentifier: "panel-8",
color: "#22B573",
fillColor: "#A6E8C4",
labelDisplayFunction: niceBytes,
},
{
title: "Total S3 Traffic Outbound",
data: [],
innerLabel: "N/A",
type: widgetType.singleRep,
layoutIdentifier: "panel-9",
color: "#22B573",
fillColor: "#A6E8C4",
labelDisplayFunction: niceBytes,
},
{
title: "Number of Buckets",
data: [],
innerLabel: "N/A",
type: widgetType.singleRep,
color: "#0071BC",
fillColor: "#ADD5E0",
layoutIdentifier: "panel-10",
},
{
title: "Number of Objects",
data: [],
innerLabel: "N/A",
type: widgetType.singleRep,
color: "#0071BC",
fillColor: "#ADD5E0",
layoutIdentifier: "panel-11",
},
{
title: "S3 API Request & Error Rate",
data: [],
widgetConfiguration: [
{
dataKey: "",
keyLabel: "",
lineColor: "#000",
fillColor: "#000",
},
],
type: widgetType.linearGraph,
layoutIdentifier: "panel-12",
disableYAxis: true,
xAxisFormatter: getTimeFromTimestamp,
},
{
title: "Total Open FDs",
data: [],
innerLabel: "N/A",
type: widgetType.singleRep,
layoutIdentifier: "panel-13",
color: "#F7655E",
fillColor: "#F4CECE",
},
{
title: "Total Goroutines",
data: [],
innerLabel: "N/A",
type: widgetType.singleRep,
layoutIdentifier: "panel-14",
color: "#F7655E",
fillColor: "#F4CECE",
},
{
title: "S3 API Data Transfer",
data: [],
widgetConfiguration: [
{
dataKey: "",
keyLabel: "",
lineColor: "#000",
fillColor: "#000",
},
],
type: widgetType.linearGraph,
layoutIdentifier: "panel-15",
disableYAxis: true,
xAxisFormatter: getTimeFromTimestamp,
},
{
title: "Total S3 API Data Transfer",
data: [],
widgetConfiguration: [
{
dataKey: "",
keyLabel: "",
lineColor: "#000",
fillColor: "#000",
},
],
type: widgetType.linearGraph,
layoutIdentifier: "panel-16",
yAxisFormatter: niceBytes,
xAxisFormatter: getTimeFromTimestamp,
},
{
title: "Active S3 Requests",
data: [],
widgetConfiguration: [
{
dataKey: "",
keyLabel: "",
lineColor: "#000",
fillColor: "#000",
},
],
type: widgetType.linearGraph,
layoutIdentifier: "panel-17",
xAxisFormatter: getTimeFromTimestamp,
},
{
title: "Internode Data Transfer",
data: [],
widgetConfiguration: [
{
dataKey: "",
keyLabel: "",
lineColor: "#000",
fillColor: "#000",
},
],
type: widgetType.linearGraph,
layoutIdentifier: "panel-18",
yAxisFormatter: niceBytes,
xAxisFormatter: getTimeFromTimestamp,
},
{
title: "Online Disks",
data: [],
widgetConfiguration: [
{
dataKey: "",
keyLabel: "",
lineColor: "#000",
fillColor: "#000",
},
],
type: widgetType.linearGraph,
layoutIdentifier: "panel-19",
disableYAxis: true,
xAxisFormatter: getTimeFromTimestamp,
},
{
title: "Disk Usage",
data: [],
widgetConfiguration: [
{
dataKey: "",
keyLabel: "",
lineColor: "#000",
fillColor: "#000",
},
],
type: widgetType.linearGraph,
layoutIdentifier: "panel-20",
yAxisFormatter: niceBytes,
xAxisFormatter: getTimeFromTimestamp,
},
];
const calculateMainValue = (elements: any[], metricCalc: string) => {
switch (metricCalc) {
case "mean":
const sumValues = elements.reduce((accumulator, currValue) => {
return accumulator + parseFloat(currValue[1]);
}, 0);
const mean = Math.floor(sumValues / elements.length);
return ["", mean.toString()];
default:
const sortResult = elements.sort(
(value1: any[], value2: any[]) => value1[0] - value2[0]
);
return sortResult[sortResult.length - 1];
}
};
const constructLabelNames = (metrics: any, legendFormat: string) => {
const keysToReplace = Object.keys(metrics);
const expToReplace = new RegExp(`{{(${keysToReplace.join("|")})}}`, "g");
const replacedLegend = legendFormat.replace(expToReplace, (matchItem) => {
const nwMatchItem = matchItem.replace(/({{|}})/g, "");
return metrics[nwMatchItem];
});
// In case not all the legends were replaced, we remove the placeholders.
return replacedLegend.replace(/{{(.*?)}}/g, "");
};
export const getWidgetsWithValue = (payload: any[]) => {
return panelsConfiguration.map((panelItem) => {
const payloadData = payload.find(
(panelT) => panelT.title === panelItem.title
);
if (!payloadData) {
return panelItem;
}
const typeOfPayload = payloadData.type;
switch (panelItem.type) {
case widgetType.singleValue:
if (typeOfPayload === "stat" || typeOfPayload === "singlestat") {
// We sort values & get the last value
const elements = get(payloadData, "targets[0].result[0].values", []);
const metricCalc = get(
payloadData,
"options.reduceOptions.calcs[0]",
"lastNotNull"
);
const valueDisplay = calculateMainValue(elements, metricCalc);
const data = panelItem.labelDisplayFunction
? panelItem.labelDisplayFunction(valueDisplay[1])
: valueDisplay[1];
return {
...panelItem,
data,
};
}
break;
case widgetType.pieChart:
if (typeOfPayload === "gauge") {
const chartSeries = get(payloadData, "targets[0].result", []);
const metricCalc = get(
payloadData,
"options.reduceOptions.calcs[0]",
"lastNotNull"
);
const totalValues = calculateMainValue(
chartSeries[0].values,
metricCalc
);
const values = chartSeries.map((elementValue: any) => {
const values = get(elementValue, "values", []);
const metricKeyItem = Object.keys(elementValue.metric);
const sortResult = values.sort(
(value1: any[], value2: any[]) => value1[0] - value2[0]
);
const metricName = elementValue.metric[metricKeyItem[0]];
const value = sortResult[sortResult.length - 1];
return { name: metricName, value: parseInt(value) };
});
const innerLabel = panelItem.labelDisplayFunction
? panelItem.labelDisplayFunction(totalValues[1])
: totalValues[1];
return {
...panelItem,
data: values,
innerLabel,
};
}
break;
case widgetType.linearGraph:
if (typeOfPayload === "graph") {
let targets = get(payloadData, "targets", []);
if (targets === null) {
targets = [];
}
const series: any[] = [];
const plotValues: any[] = [];
targets.forEach(
(
targetMaster: { legendFormat: string; result: any[] },
index: number
) => {
// Add a new serie to plot variables in case it is not from multiple values
let results = get(targetMaster, "result", []);
const legendFormat = targetMaster.legendFormat;
if (results === null) {
results = [];
}
results.forEach((itemVals: { metric: object; values: any[] }) => {
// Label Creation
const labelName = constructLabelNames(
itemVals.metric,
legendFormat
);
const keyName = `key_${index}${labelName}`;
// series creation with recently created label
series.push({
dataKey: keyName,
keyLabel: labelName,
lineColor: "",
fillColor: "",
});
// we iterate over values and create elements
let values = get(itemVals, "values", []);
if (values === null) {
values = [];
}
values.forEach((valInfo: any[]) => {
const itemIndex = plotValues.findIndex(
(element) => element.name === valInfo[0]
);
// Element not exists yet
if (itemIndex === -1) {
let itemToPush: any = { name: valInfo[0] };
itemToPush[keyName] = valInfo[1];
plotValues.push(itemToPush);
} else {
plotValues[itemIndex][keyName] = valInfo[1];
}
});
});
}
);
const sortedSeries = series.sort((series1: any, series2: any) => {
if (series1.keyLabel < series2.keyLabel) {
return -1;
}
if (series1.keyLabel > series2.keyLabel) {
return 1;
}
return 0;
});
const seriesWithColors = sortedSeries.map(
(serialC: any, index: number) => {
return {
...serialC,
lineColor:
colorsMain[index] || textToRGBColor(serialC.keyLabel),
fillColor:
colorsMain[index] || textToRGBColor(serialC.keyLabel),
};
}
);
const sortedVals = plotValues.sort(
(value1: any, value2: any) => value1.name - value2.name
);
return {
...panelItem,
widgetConfiguration: seriesWithColors,
data: sortedVals,
};
}
break;
case widgetType.barChart:
if (typeOfPayload === "bargauge") {
const chartBars = get(payloadData, "targets[0].result", []);
const values = chartBars.map((elementValue: any) => {
const metricKeyItem = Object.keys(elementValue.metric);
const metricName = elementValue.metric[metricKeyItem[0]];
const elements = get(elementValue, "values", []);
const sortResult = elements.sort(
(value1: any[], value2: any[]) => value1[0] - value2[0]
);
const lastValue = sortResult[sortResult.length - 1];
return { name: metricName, a: parseInt(lastValue[1]) };
});
return {
...panelItem,
data: values,
};
}
break;
case widgetType.singleRep:
if (typeOfPayload === "stat") {
// We sort values & get the last value
let elements = get(payloadData, "targets[0].result[0].values", []);
if (elements === null) {
elements = [];
}
const metricCalc = get(
payloadData,
"options.reduceOptions.calcs[0]",
"lastNotNull"
);
const valueDisplay = calculateMainValue(elements, metricCalc);
const sortResult = elements.sort(
(value1: any[], value2: any[]) => value1[0] - value2[0]
);
let valuesForBackground = [];
if (sortResult.length === 1) {
valuesForBackground.push({ value: 0 });
}
sortResult.forEach((eachVal: any) => {
valuesForBackground.push({ value: parseInt(eachVal[1]) });
});
const innerLabel = panelItem.labelDisplayFunction
? panelItem.labelDisplayFunction(valueDisplay[1])
: valueDisplay[1];
return {
...panelItem,
data: valuesForBackground,
innerLabel,
};
}
break;
}
return panelItem;
});
};
export const saveDashboardDistribution = (configuration: Layout[]) => {
localStorage.setItem(dLocalStorageV, btoa(JSON.stringify(configuration)));
};
export const getDashboardDistribution = () => {
const storedConfiguration = localStorage.getItem(dLocalStorageV);
if (!storedConfiguration) {
return defaultWidgetsLayout;
}
return JSON.parse(atob(storedConfiguration));
};

View File

@@ -18,4 +18,5 @@ export interface Usage {
usage: number;
buckets: number;
objects: number;
widgets?: any;
}

View File

@@ -198,7 +198,6 @@ const License = ({ classes, operatorMode }: ILicenseProps) => {
</Grid>
);
}
console.log("operatorMode", operatorMode);
return (
<React.Fragment>
{licenseInfo ? (

View File

@@ -15,10 +15,10 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export interface LicenseInfo {
account_id: number,
account_id: number;
email: string;
expires_at: string;
plan: string;
storage_capacity: number,
storage_capacity: number;
organization: string;
}

View File

@@ -13,35 +13,35 @@
//
// 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 React, { Fragment, useState, 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 { wsProtocol } from "../../../utils/wsUtils";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import { connect } from "react-redux";
import { Grid } from "@material-ui/core";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import PageHeader from "../Common/PageHeader/PageHeader";
import moment from "moment/moment";
import { AppState } from "../../../../store";
import { logMessageReceived, logResetMessages } from "../actions";
import { LogMessage } from "../types";
import { timeFromDate } from "../../../../common/utils";
import { wsProtocol } from "../../../../utils/wsUtils";
import {
actionsTray,
logsCommon,
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
logList: {
background: "#fff",
minHeight: 400,
height: "calc(100vh - 270px)",
height: "calc(100vh - 304px)",
overflow: "auto",
fontSize: 13,
padding: "25px 45px",
padding: "25px 45px 0",
border: "1px solid #EAEDEE",
borderRadius: 4,
},
@@ -65,7 +65,7 @@ const styles = (theme: Theme) =>
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
...logsCommon,
});
interface ILogs {
@@ -73,11 +73,9 @@ interface ILogs {
logMessageReceived: typeof logMessageReceived;
logResetMessages: typeof logResetMessages;
messages: LogMessage[];
namespace: string;
tenant: string;
}
const Logs = ({
const ErrorLogs = ({
classes,
logMessageReceived,
logResetMessages,
@@ -108,8 +106,9 @@ const Logs = ({
};
c.onmessage = (message: IMessageEvent) => {
// console.log(message.data.toString())
// FORMAT: 00:35:17 UTC 01/01/2021
let m: LogMessage = JSON.parse(message.data.toString());
m.time = new Date(m.time.toString());
m.time = moment(m.time, "HH:mm:s UTC MM/DD/YYYY").toDate();
m.key = Math.random();
logMessageReceived(m);
};
@@ -323,10 +322,8 @@ const Logs = ({
});
return (
<React.Fragment>
<PageHeader label="Logs" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Fragment>
<Grid container className={classes.logsSubContainer}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Highlight Line"
@@ -353,8 +350,7 @@ const Logs = ({
<div className={classes.logList}>{renderLines}</div>
</Grid>
</Grid>
</Grid>
</React.Fragment>
</Fragment>
);
};
@@ -367,4 +363,4 @@ const connector = connect(mapState, {
logResetMessages: logResetMessages,
});
export default connector(withStyles(styles)(Logs));
export default withStyles(styles)(connector(ErrorLogs));

View File

@@ -0,0 +1,427 @@
// 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, { Fragment, useState, useEffect, useCallback } from "react";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, Grid } from "@material-ui/core";
import { ArrowDropUp } from "@material-ui/icons";
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import ErrorBlock from "../../../shared/ErrorBlock";
import {
actionsTray,
containerForHeader,
logsCommon,
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
import { IReqInfoSearchResults, ISearchResponse } from "./types";
import { niceBytes, nsToSeconds } from "../../../../common/utils";
import api from "../../../../common/api";
import FilterInputWrapper from "../../Common/FormComponents/FilterInputWrapper/FilterInputWrapper";
import DateTimePickerWrapper from "../../Common/FormComponents/DateTimePickerWrapper/DateTimePickerWrapper";
interface ILogSearchProps {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
inputBar: {
flexGrow: 1,
marginLeft: 15,
},
advancedLabel: {
display: "flex",
alignItems: "center",
color: "#091C42",
border: 0,
backgroundColor: "transparent",
cursor: "pointer",
"&:focus, &:active": {
outline: "none",
},
},
advancedLabelContainer: {
marginTop: 10,
},
getInformationContainer: {
textAlign: "right",
},
orderButton: {
width: 93,
},
recordsLabel: {
alignSelf: "center",
marginLeft: 15,
},
blockCollapsed: {
height: 0,
overflowY: "hidden",
transitionDuration: "0.3s",
},
filterOpen: {
height: 200,
marginBottom: 12,
},
endLineAction: {
marginBottom: 15,
},
filtersContainer: {
display: "flex",
justifyContent: "space-between",
marginBottom: 12,
},
innerContainer: {
backgroundColor: "#fff",
border: "#EAEDEE 1px solid",
borderRadius: 3,
padding: 10,
marginBottom: 15,
},
noticeLabel: {
marginLeft: 15,
marginBottom: 15,
fontSize: 12,
color: "#9C9C9C",
},
tableFOpen: {
height: "calc(100vh - 561px)",
},
tableFClosed: {
height: "calc(100vh - 349px)",
},
"@global": {
".overrideMargin": {
marginLeft: 0,
},
},
...searchField,
...actionsTray,
...logsCommon,
...containerForHeader(theme.spacing(4)),
});
const LogsSearchMain = ({ classes }: ILogSearchProps) => {
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(true);
const [timeStart, setTimeStart] = useState<any>(null);
const [timeEnd, setTimeEnd] = useState<any>(null);
const [filterOpen, setFilterOpen] = useState<boolean>(false);
const [records, setRecords] = useState<IReqInfoSearchResults[]>([]);
const [bucket, setBucket] = useState<string>("");
const [apiName, setApiName] = useState<string>("");
const [userAgent, setUserAgent] = useState<string>("");
const [object, setObject] = useState<string>("");
const [requestID, setRequestID] = useState<string>("");
const [responseStatus, setResponseStatus] = useState<string>("");
const [sortOrder, setSortOrder] = useState<"ASC" | "DESC" | undefined>(
"DESC"
);
const [columnsShown, setColumnsShown] = useState<string[]>([
"time",
"api_name",
"bucket",
"object",
"remote_host",
"request_id",
"user_agent",
"response_status",
]);
const [nextPage, setNextPage] = useState<number>(0);
const [alreadyFetching, setAlreadyFetching] = useState<boolean>(false);
let recordsResp: any = null;
const fetchRecords = useCallback(() => {
if (!alreadyFetching) {
setAlreadyFetching(true);
let queryParams = `${bucket !== "" ? `&fp=bucket:${bucket}` : ""}${
object !== "" ? `&fp=object:${object}` : ""
}${apiName !== "" ? `&fp=api_name:${apiName}` : ""}${
requestID !== "" ? `&fp=request_id:${requestID}` : ""
}${userAgent !== "" ? `&fp=user_agent:${userAgent}` : ""}${
responseStatus !== "" ? `&fp=response_status:${responseStatus}` : ""
}`;
queryParams = queryParams.trim();
if (queryParams.endsWith(",")) {
queryParams = queryParams.slice(0, -1);
}
api
.invoke(
"GET",
`/api/v1/logs/search?q=reqinfo${
queryParams !== "" ? `${queryParams}` : ""
}&pageSize=100&pageNo=${nextPage}&order=${
sortOrder === "DESC" ? "timeDesc" : "timeAsc"
}${
timeStart !== null ? `&timeStart=${timeStart.toISOString()}` : ""
}${timeEnd !== null ? `&timeEnd=${timeEnd.toISOString()}` : ""}`
)
.then((res: ISearchResponse) => {
const fetchedResults = res.results || [];
const newResultSet = [...records, ...fetchedResults];
setLoading(false);
setAlreadyFetching(false);
setRecords(newResultSet);
setError("");
setNextPage(nextPage + 1);
if (recordsResp !== null) {
recordsResp();
}
})
.catch((err: any) => {
setLoading(false);
setAlreadyFetching(false);
setError(err);
});
}
}, [
bucket,
object,
apiName,
requestID,
userAgent,
responseStatus,
nextPage,
sortOrder,
timeStart,
timeEnd,
alreadyFetching,
records,
recordsResp,
]);
useEffect(() => {
if (loading) {
setRecords([]);
fetchRecords();
}
}, [loading, sortOrder, fetchRecords]);
const triggerLoad = () => {
setNextPage(0);
setLoading(true);
};
const selectColumn = (colName: string, active: boolean) => {
let newArray = [...columnsShown];
if (!active) {
newArray = columnsShown.filter((element) => element !== colName);
} else {
if (!newArray.includes(colName)) {
newArray.push(colName);
}
}
setColumnsShown(newArray);
};
const sortChange = (sortData: any) => {
const newSortDirection = get(sortData, "sortDirection", "DESC");
setSortOrder(newSortDirection);
setNextPage(0);
setLoading(true);
};
const loadMoreRecords = (_: { startIndex: number; stopIndex: number }) => {
fetchRecords();
return new Promise((resolve) => {
recordsResp = resolve;
});
};
return (
<Fragment>
<Grid container className={classes.logsSubContainer}>
{error !== "" && (
<Grid item xs={12}>
<ErrorBlock errorMessage={error} />
</Grid>
)}
<Grid
item
xs={12}
className={`${classes.actionsTray} ${classes.timeContainers}`}
>
<span className={classes.label}>Start Time</span>
<DateTimePickerWrapper value={timeStart} onChange={setTimeStart} />
<span className={classes.label}>End Time</span>
<DateTimePickerWrapper value={timeEnd} onChange={setTimeEnd} />
</Grid>
<Grid item xs={12} className={`${classes.advancedLabelContainer}`}>
<div
className={`${classes.blockCollapsed} ${
filterOpen ? classes.filterOpen : ""
}`}
>
<div className={classes.innerContainer}>
<div className={classes.noticeLabel}>
Enable your preferred options to get filtered records.
<br />
You can use '*' to match any character, '.' to signify a single
character or '\' to scape an special character (E.g. mybucket-*)
</div>
<div className={classes.filtersContainer}>
<FilterInputWrapper
onChange={setBucket}
value={bucket}
label={"Bucket"}
id="bucket"
name="bucket"
/>
<FilterInputWrapper
onChange={setApiName}
value={apiName}
label={"API Name"}
id="api_name"
name="api_name"
/>
<FilterInputWrapper
onChange={setUserAgent}
value={userAgent}
label={"User Agent"}
id="user_agent"
name="user_agent"
/>
</div>
<div className={classes.filtersContainer}>
<FilterInputWrapper
onChange={setObject}
value={object}
label={"Object"}
id="object"
name="object"
/>
<FilterInputWrapper
onChange={setRequestID}
value={requestID}
label={"Request ID"}
id="request_id"
name="request_id"
/>
<FilterInputWrapper
onChange={setResponseStatus}
value={responseStatus}
label={"Response Status"}
id="response_status"
name="response_status"
/>
</div>
</div>
</div>
</Grid>
<Grid
item
xs={12}
className={`${classes.actionsTray} ${classes.endLineAction}`}
>
<div>
<button
type="button"
className={`${classes.advancedLabel} overrideMargin`}
onClick={() => {
setFilterOpen(!filterOpen);
}}
>
Advanced Filters{" "}
{filterOpen ? <ArrowDropUp /> : <ArrowDropDownIcon />}
</button>
</div>
<Button
type="button"
variant="contained"
color="primary"
onClick={triggerLoad}
>
Get Information
</Button>
</Grid>
<Grid item xs={12}>
<TableWrapper
columns={[
{ label: "Timestamp", elementKey: "time", enableSort: true },
{ label: "API Name", elementKey: "api_name" },
{ label: "Bucket", elementKey: "bucket" },
{ label: "Object", elementKey: "object" },
{ label: "Remote Host", elementKey: "remote_host" },
{ label: "Request ID", elementKey: "request_id" },
{ label: "User Agent", elementKey: "user_agent" },
{
label: "Response Status",
elementKey: "response_status",
renderFunction: (element) => (
<Fragment>
<span>
{element.response_status_code} ({element.response_status})
</span>
</Fragment>
),
renderFullObject: true,
},
{
label: "Request Content Length",
elementKey: "request_content_length",
renderFunction: niceBytes,
},
{
label: "Response Content Length",
elementKey: "response_content_length",
renderFunction: niceBytes,
},
{
label: "Time to Response NS",
elementKey: "time_to_response_ns",
renderFunction: nsToSeconds,
contentTextAlign: "right",
},
]}
isLoading={loading}
records={records}
entityName="Logs"
customEmptyMessage={"There is no information with this criteria"}
idField="request_id"
columnsSelector
columnsShown={columnsShown}
onColumnChange={selectColumn}
customPaperHeight={
filterOpen ? classes.tableFOpen : classes.tableFClosed
}
sortConfig={{
currentSort: "time",
currentDirection: sortOrder,
triggerSort: sortChange,
}}
infiniteScrollConfig={{
recordsCount: 1000000,
loadMoreRecords: loadMoreRecords,
}}
textSelectable
/>
</Grid>
</Grid>
</Fragment>
);
};
export default withStyles(styles)(LogsSearchMain);

View File

@@ -0,0 +1,34 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export interface IReqInfoSearchResults {
id: string;
api_name: string;
bucket: string;
object: string;
time_to_response_ns: number;
remote_host: string;
request_id: string;
user_agent: string;
response_status: string;
response_status_code: number;
request_content_length: any;
response_content_length: any;
}
export interface ISearchResponse {
results: IReqInfoSearchResults[];
}

View File

@@ -0,0 +1,66 @@
import React, { Fragment, useState } from "react";
import PageHeader from "../Common/PageHeader/PageHeader";
import { Grid } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Tab from "@material-ui/core/Tab";
import Tabs from "@material-ui/core/Tabs";
import ErrorLogs from "./ErrorLogs/ErrorLogs";
import LogsSearchMain from "./LogSearch/LogsSearchMain";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
interface ILogsMainProps {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
headerLabel: {
fontSize: 22,
fontWeight: 600,
color: "#000",
marginTop: 4,
},
...containerForHeader(theme.spacing(4)),
});
const LogsMain = ({ classes }: ILogsMainProps) => {
const [currentTab, setCurrentTab] = useState<number>(0);
return (
<Fragment>
<PageHeader label="Logs" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.headerLabel}>
All Logs
</Grid>
<Tabs
value={currentTab}
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
setCurrentTab(newValue);
}}
indicatorColor="primary"
textColor="primary"
aria-label="cluster-tabs"
>
<Tab label="Error Logs" />
<Tab label="Logs Search" />
</Tabs>
</Grid>
<Grid item xs={12}>
{currentTab === 0 && (
<Grid item xs={12}>
<ErrorLogs />
</Grid>
)}
{currentTab === 1 && (
<Grid item xs={12}>
<LogsSearchMain />
</Grid>
)}
</Grid>
</Grid>
</Fragment>
);
};
export default withStyles(styles)(LogsMain);

View File

@@ -37,6 +37,7 @@ import {
getBytes,
k8sfactorForDropdown,
niceBytes,
setMemoryResource,
} from "../../../../common/utils";
import {
commonFormValidation,
@@ -54,7 +55,14 @@ import {
IErasureCodeCalc,
ITenantCreator,
} from "../../../../common/types";
import { ecListTransform, Opts } from "./utils";
import {
ecListTransform,
getLimitSizes,
IQuotaElement,
IQuotas,
Opts,
} from "./utils";
import { IMemorySize } from "./types";
interface IAddTenantProps {
open: boolean;
@@ -168,6 +176,13 @@ const AddTenant = ({
const [drivesPerServer, setDrivesPerServer] = useState<string>("1");
const [memoryNode, setMemoryNode] = useState<string>("2");
const [ecParity, setECParity] = useState<string>("");
const [maxAllocableMemo, setMaxAllocableMemo] = useState<number>(0);
const [limitSize, setLimitSize] = useState<any>({});
const [memorySize, setMemorySize] = useState<IMemorySize>({
error: "",
limit: 0,
request: 0,
});
const [distribution, setDistribution] = useState<any>({
error: "",
nodes: 0,
@@ -183,6 +198,8 @@ const AddTenant = ({
rawCapacity: "0",
storageFactors: [],
});
const [exposeMinIO, setExposeMinIO] = useState<boolean>(true);
const [exposeConsole, setExposeConsole] = useState<boolean>(true);
// Forms Validation
const [nameTenantValid, setNameTenantValid] = useState<boolean>(false);
@@ -244,8 +261,9 @@ const AddTenant = ({
"GET",
`/api/v1/namespaces/${namespace}/resourcequotas/${namespace}-storagequota`
)
.then((res: string[]) => {
const elements = get(res, "elements", []);
.then((res: IQuotas) => {
const elements: IQuotaElement[] = get(res, "elements", []);
setLimitSize(getLimitSizes(res));
const newStorage = elements.map((storageClass: any) => {
const name = get(storageClass, "name", "").split(
@@ -266,6 +284,47 @@ const AddTenant = ({
});
};
const validateMemorySize = useCallback(() => {
const memSize = parseInt(memoryNode) || 0;
const clusterSize = volumeSize || 0;
const maxMemSize = maxAllocableMemo || 0;
const clusterSizeFactor = sizeFactor;
const clusterSizeBytes = getBytes(
clusterSize.toString(10),
clusterSizeFactor
);
const memoSize = setMemoryResource(memSize, clusterSizeBytes, maxMemSize);
setMemorySize(memoSize);
}, [maxAllocableMemo, memoryNode, sizeFactor, volumeSize]);
const getMaxAllocableMemory = (nodes: string) => {
if (nodes !== "" && !isNaN(parseInt(nodes))) {
api
.invoke(
"GET",
`/api/v1/cluster/max-allocatable-memory?num_nodes=${nodes}`
)
.then((res: { max_memory: number }) => {
const maxMemory = res.max_memory ? res.max_memory : 0;
setMaxAllocableMemo(maxMemory);
})
.catch((err: any) => {
setMaxAllocableMemo(0);
console.error(err);
});
}
};
useEffect(() => {
validateMemorySize();
}, [memoryNode, validateMemorySize]);
useEffect(() => {
validateMemorySize();
}, [maxAllocableMemo, validateMemorySize]);
const debounceNamespace = useCallback(
debounce(getNamespaceInformation, 500),
[namespace]
@@ -299,6 +358,7 @@ const AddTenant = ({
useEffect(() => {
validateClusterSize();
getECValue();
getMaxAllocableMemory(nodes);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodes, volumeSize, sizeFactor, drivesPerServer]);
@@ -355,7 +415,7 @@ const AddTenant = ({
{
fieldKey: "namespace",
required: true,
value: tenantName,
value: namespace,
customValidation: storageClasses.length < 1,
customValidationMessage: "Please enter a valid namespace",
},
@@ -385,8 +445,13 @@ const AddTenant = ({
fieldKey: "volume_size",
required: true,
value: volumeSize,
customValidation: parseInt(parsedSize) < 1073741824,
customValidationMessage: "Volume size must be greater than 1Gi",
customValidation:
parseInt(parsedSize) < 1073741824 ||
parseInt(parsedSize) > limitSize[selectedStorageClass],
customValidationMessage: `Volume size must be greater than 1Gi and less than ${niceBytes(
limitSize[selectedStorageClass],
true
)}`,
},
{
fieldKey: "memory_per_node",
@@ -410,7 +475,8 @@ const AddTenant = ({
!("memory_per_node" in commonValidation) &&
!("drivesps" in commonValidation) &&
distribution.error === "" &&
ecParityCalc.error === 0
ecParityCalc.error === 0 &&
memorySize.error === ""
);
setValidationErrors(commonValidation);
@@ -422,6 +488,9 @@ const AddTenant = ({
distribution,
drivesPerServer,
ecParityCalc,
memorySize,
limitSize,
selectedStorageClass,
]);
useEffect(() => {
@@ -830,6 +899,8 @@ const AddTenant = ({
service_name: "",
image: imageName,
console_image: consoleImage,
expose_minio: exposeMinIO,
expose_console: exposeConsole,
pools: [
{
name: poolName,
@@ -841,7 +912,10 @@ const AddTenant = ({
},
resources: {
requests: {
memory: parseInt(getBytes(memoryNode, "GiB")),
memory: memorySize.request,
},
limits: {
memory: memorySize.limit,
},
},
affinity: hardCodedAffinity,
@@ -1260,6 +1334,42 @@ const AddTenant = ({
</Grid>
</React.Fragment>
)}
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Expose Services</h3>
<span className={classes.descriptionText}>
Whether the tenant's services should request an external IP.
</span>
</div>
<Grid item xs={12}>
<FormSwitchWrapper
value="expose_minio"
id="expose_minio"
name="expose_minio"
checked={exposeMinIO}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
setExposeMinIO(checked);
}}
label={"Expose MiniO Service"}
/>
</Grid>
<Grid item xs={12}>
<FormSwitchWrapper
value="expose_console"
id="expose_console"
name="expose_console"
checked={exposeConsole}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
setExposeConsole(checked);
}}
label={"Expose Console Service"}
/>
</Grid>
</React.Fragment>
),
buttons: [
@@ -2082,6 +2192,7 @@ const AddTenant = ({
</span>
</div>
<span className={classes.error}>{distribution.error}</span>
<span className={classes.error}>{memorySize.error}</span>
<Grid item xs={12}>
<InputBoxWrapper
id="nodes"
@@ -2125,7 +2236,7 @@ const AddTenant = ({
setVolumeSize(e.target.value);
clearValidationError("volume_size");
}}
label="Size"
label="Total Size"
value={volumeSize}
required
error={validationErrors["volume_size"] || ""}
@@ -2146,6 +2257,8 @@ const AddTenant = ({
</div>
</div>
</Grid>
{advancedMode && (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
type="number"
@@ -2162,7 +2275,6 @@ const AddTenant = ({
min="2"
/>
</Grid>
{advancedMode && (
<Grid item xs={12}>
<SelectWrapper
id="ec_parity"
@@ -2179,6 +2291,7 @@ const AddTenant = ({
max usable capacity in the cluster
</span>
</Grid>
</React.Fragment>
)}
<h4>Resource Allocation</h4>
<Table className={classes.table} aria-label="simple table">
@@ -2215,6 +2328,14 @@ const AddTenant = ({
{distribution ? distribution.persistentVolumes : "-"}
</TableCell>
</TableRow>
{!advancedMode && (
<TableRow>
<TableCell component="th" scope="row">
Memory per Node
</TableCell>
<TableCell align="right">{memoryNode} Gi</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{ecParityCalc.error === 0 && usableInformation && (
@@ -2323,7 +2444,7 @@ const AddTenant = ({
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Volume Size
Total Size
</TableCell>
<TableCell>
{volumeSize} {sizeFactor}

View File

@@ -39,6 +39,11 @@ export interface IVolumeConfiguration {
labels: { [key: string]: any } | null;
}
export interface IEndpoints {
minio: string;
console: string;
}
export interface ITenant {
total_size: number;
name: string;
@@ -53,6 +58,7 @@ export interface ITenant {
volume_count: number;
volumes_per_server: number;
pools: IPool[];
endpoints: IEndpoints;
// computed
capacity: string;
subnet_license: LicenseInfo;
@@ -61,3 +67,9 @@ export interface ITenant {
export interface ITenantsResponse {
tenants: ITenant[];
}
export interface IMemorySize {
error: string;
limit: number;
request: number;
}

View File

@@ -14,13 +14,45 @@
// 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 get from "lodash/get";
export interface Opts {
label: string;
value: string;
}
export interface IQuotaElement {
hard: number;
name: string;
}
export interface IQuotas {
elements?: IQuotaElement[];
name: string;
}
export const minMemReq = 2147483648;
export const ecListTransform = (ecList: string[]): Opts[] => {
return ecList.map((value) => {
return { label: value, value };
});
};
export const getLimitSizes = (resourceQuotas: IQuotas) => {
const quotas: IQuotaElement[] = get(resourceQuotas, "elements", []);
const returnQuotas: any = {};
quotas.forEach((rsQuota) => {
const stCName = rsQuota.name.split(
".storageclass.storage.k8s.io/requests.storage"
)[0];
const hard = get(rsQuota, "hard", 0);
const used = get(rsQuota, "used", 0);
returnQuotas[stCName] = hard - used;
});
return returnQuotas;
};

View File

@@ -329,6 +329,30 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
<div>{instances}</div>
<div>Volumes:</div>
<div>{volumes}</div>
{tenant?.endpoints && (
<React.Fragment>
<div>Endpoint:</div>
<div>
<a
href={tenant?.endpoints.minio}
target="_blank"
rel="noopener noreferrer"
>
{tenant?.endpoints.minio}
</a>
</div>
<div>Console:</div>
<div>
<a
href={tenant?.endpoints.console}
target="_blank"
rel="noopener noreferrer"
>
{tenant?.endpoints.console}
</a>
</div>
</React.Fragment>
)}
</div>
</Paper>
</Grid>

View File

@@ -27,6 +27,7 @@ import { containerForHeader } from "../Common/FormComponents/common/styleLibrary
import { Grid } from "@material-ui/core";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import PageHeader from "../Common/PageHeader/PageHeader";
import moment from "moment/moment";
const styles = (theme: Theme) =>
createStyles({
@@ -89,7 +90,7 @@ const Trace = ({
};
c.onmessage = (message: IMessageEvent) => {
let m: TraceMessage = JSON.parse(message.data.toString());
m.time = new Date(m.time.toString());
m.ptime = moment(m.time, "YYYY-MM-DD HH:mm:s.SSSS +0000 UTC").toDate();
m.key = Math.random();
traceMessageReceived(m);
};
@@ -115,7 +116,7 @@ const Trace = ({
columns={[
{
label: "Time",
elementKey: "time",
elementKey: "ptime",
renderFunction: (time: Date) => {
const timeParse = new Date(time);
return timeFromDate(timeParse);
@@ -160,6 +161,7 @@ const Trace = ({
entityName="Traces"
idField="api"
customEmptyMessage="There are no traced Elements yet"
autoScrollToBottom
/>
</Grid>
</Grid>

View File

@@ -23,7 +23,8 @@ export interface CallStats {
export interface TraceMessage {
client: string;
time: Date;
time: string;
ptime: Date;
statusCode: number;
api: string;
query: string;

View File

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

View File

@@ -1090,7 +1090,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
@@ -1148,6 +1148,18 @@
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
"@date-io/core@1.x", "@date-io/core@^1.3.13":
version "1.3.13"
resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa"
integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==
"@date-io/moment@1.x":
version "1.3.13"
resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-1.3.13.tgz#56c2772bc4f6675fc6970257e6033e7a7c2960f0"
integrity sha512-3kJYusJtQuOIxq6byZlzAHoW/18iExJer9qfRF5DyyzdAk074seTuJfdofjz4RFfTd/Idk8WylOQpWtERqvFuQ==
dependencies:
"@date-io/core" "^1.3.13"
"@emotion/hash@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
@@ -1367,6 +1379,18 @@
dependencies:
"@babel/runtime" "^7.4.4"
"@material-ui/pickers@^3.2.10":
version "3.2.10"
resolved "https://registry.yarnpkg.com/@material-ui/pickers/-/pickers-3.2.10.tgz#19df024895876eb0ec7cd239bbaea595f703f0ae"
integrity sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w==
dependencies:
"@babel/runtime" "^7.6.0"
"@date-io/core" "1.x"
"@types/styled-jsx" "^2.2.8"
clsx "^1.0.2"
react-transition-group "^4.0.0"
rifm "^0.7.0"
"@material-ui/styles@^4.11.2":
version "4.11.2"
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.2.tgz#e70558be3f41719e8c0d63c7a3c9ae163fdc84cb"
@@ -1686,6 +1710,13 @@
dependencies:
"@types/react" "*"
"@types/react-grid-layout@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/react-grid-layout/-/react-grid-layout-1.1.1.tgz#1eb322fa88cd1475bdc60ecca9676f6853e18887"
integrity sha512-bvPkITzwGGOZKjp01nVSgPrdfGm/uTa5t8Odd8vQRXJsLj7uZLZXSXgWr+TiXBAkUsmHPxhsyswXQCiFeDuZnQ==
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.5":
version "7.1.12"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.12.tgz#148f2c768687346b556e29a322ca44cfa28cc3ac"
@@ -1749,6 +1780,13 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
"@types/styled-jsx@^2.2.8":
version "2.2.8"
resolved "https://registry.yarnpkg.com/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234"
integrity sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg==
dependencies:
"@types/react" "*"
"@types/superagent@^4.1.4":
version "4.1.10"
resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.10.tgz#5e2cc721edf58f64fe9b819f326ee74803adee86"
@@ -3074,7 +3112,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2.5:
classnames@2.x, classnames@^2.2.5:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
@@ -3137,7 +3175,7 @@ clone-deep@^4.0.1:
kind-of "^6.0.2"
shallow-clone "^3.0.0"
clsx@^1.0.4:
clsx@^1.0.2, clsx@^1.0.4:
version "1.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
@@ -7015,6 +7053,11 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
lodash.isequal@^4.0.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -7379,7 +7422,7 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
dependencies:
minimist "^1.2.5"
moment@^2.10.2, moment@^2.24.0:
moment@^2.10.2, moment@^2.29.1:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
@@ -8921,7 +8964,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@15.x, prop-types@^15.0.0, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -9169,11 +9212,30 @@ react-dom@17.0.1:
object-assign "^4.1.1"
scheduler "^0.20.1"
react-draggable@^4.0.0, react-draggable@^4.0.3:
version "4.4.3"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.3.tgz#0727f2cae5813e36b0e4962bf11b2f9ef2b406f3"
integrity sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==
dependencies:
classnames "^2.2.5"
prop-types "^15.6.0"
react-error-overlay@^6.0.7:
version "6.0.8"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de"
integrity sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==
react-grid-layout@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.2.0.tgz#87124d549c86c8df8841666618c8c3e3cb205c26"
integrity sha512-fJMGQFguphkAs0NsLNf8hz9cUv9B642JYei2yddiPby/X/kJ4HFIaMUhhqg1ArVfn/vHet1+h+LE4n85cFPh+Q==
dependencies:
classnames "2.x"
lodash.isequal "^4.0.0"
prop-types "^15.0.0"
react-draggable "^4.0.0"
react-resizable "^1.10.0"
react-hot-loader@^4.13.0:
version "4.13.0"
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.13.0.tgz#c27e9408581c2a678f5316e69c061b226dc6a202"
@@ -9219,6 +9281,14 @@ react-redux@^7.1.3:
prop-types "^15.7.2"
react-is "^16.13.1"
react-resizable@^1.10.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.11.0.tgz#0b237c4aff16937b7663de1045861749683227ad"
integrity sha512-VoGz2ddxUFvildS8r8/29UZJeyiM3QJnlmRZSuXm+FpTqq/eIrMPc796Y9XQLg291n2hFZJtIoP1xC3hSTw/jg==
dependencies:
prop-types "15.x"
react-draggable "^4.0.3"
react-resize-detector@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-2.3.0.tgz#57bad1ae26a28a62a2ddb678ba6ffdf8fa2b599c"
@@ -9338,7 +9408,7 @@ react-transition-group@^2.5.0:
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react-transition-group@^4.4.0:
react-transition-group@^4.0.0, react-transition-group@^4.4.0:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
@@ -9793,6 +9863,13 @@ rgba-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
rifm@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be"
integrity sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==
dependencies:
"@babel/runtime" "^7.3.1"
rimraf@2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"

View File

@@ -18,6 +18,14 @@ package restapi
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/go-openapi/runtime/middleware"
@@ -29,7 +37,7 @@ import (
func registerAdminInfoHandlers(api *operations.ConsoleAPI) {
// return usage stats
api.AdminAPIAdminInfoHandler = admin_api.AdminInfoHandlerFunc(func(params admin_api.AdminInfoParams, session *models.Principal) middleware.Responder {
infoResp, err := getAdminInfoResponse(session)
infoResp, err := getAdminInfoResponse(session, params)
if err != nil {
return admin_api.NewAdminInfoDefault(int(err.Code)).WithPayload(err)
}
@@ -69,8 +77,387 @@ func getAdminInfo(ctx context.Context, client MinioAdmin) (*usageInfo, error) {
}, nil
}
type Target struct {
Expr string
Interval string
LegendFormat string
}
type ReduceOptions struct {
Calcs []string
}
type MetricOptions struct {
ReduceOptions ReduceOptions
}
type Metric struct {
Title string
Type string
Options MetricOptions
Targets []Target
}
type WidgetLabel struct {
Name string
}
var labels = []WidgetLabel{
{Name: "instance"},
{Name: "disk"},
}
var widgets = []Metric{
{
Title: "Uptime",
Type: "stat",
Options: MetricOptions{
ReduceOptions: ReduceOptions{
Calcs: []string{
"mean",
},
},
},
Targets: []Target{
{
Expr: "time() - max(process_start_time_seconds)",
LegendFormat: "{{instance}}",
},
},
},
{
Title: "Total Online disks",
Type: "stat",
Options: MetricOptions{
ReduceOptions: ReduceOptions{
Calcs: []string{
"mean",
},
},
},
Targets: []Target{
{
Expr: "sum(minio_disks_total)",
LegendFormat: "Total online disks in MinIO Cluster",
},
},
},
{
Title: "Total Data",
Type: "gauge",
Options: MetricOptions{
ReduceOptions: ReduceOptions{
Calcs: []string{
"lastNotNull",
},
},
},
Targets: []Target{
{
Expr: "topk(1, sum(bucket_usage_size) by (instance))",
LegendFormat: "",
},
},
},
{
Title: "Data Growth",
Type: "graph",
Targets: []Target{
{
Expr: "topk(1, sum(bucket_usage_size) by (instance))",
LegendFormat: "Total Storage Used",
},
},
},
{
Title: "Object size distribution",
Type: "bargauge",
Options: MetricOptions{
ReduceOptions: ReduceOptions{
Calcs: []string{
"mean",
},
},
},
Targets: []Target{
{
Expr: "max by (object_size) (bucket_objects_histogram)",
LegendFormat: "{{object_size}}",
},
},
},
{
Title: "Total Offline disks",
Type: "singlestat",
Targets: []Target{
{
Expr: "sum(minio_disks_offline)",
LegendFormat: "Total offline disks in MinIO Cluster",
},
},
},
{
Title: "Total Online Servers",
Type: "stat",
Options: MetricOptions{
ReduceOptions: ReduceOptions{
Calcs: []string{
"mean",
},
},
},
Targets: []Target{
{
Expr: "count by (instances) (minio_version_info)",
LegendFormat: "",
},
},
},
{
Title: "Total S3 Traffic Inbound",
Type: "stat",
Options: MetricOptions{
ReduceOptions: ReduceOptions{
Calcs: []string{
"mean",
},
},
},
Targets: []Target{
{
Expr: "sum without (instance) (s3_rx_bytes_total)",
LegendFormat: "",
},
},
},
{
Title: "Number of Buckets",
Type: "stat",
Options: MetricOptions{
ReduceOptions: ReduceOptions{
Calcs: []string{
"lastNotNull",
},
},
},
Targets: []Target{
{
Expr: "count(count by (bucket) (bucket_objects_count))",
LegendFormat: "",
},
},
},
{
Title: "S3 API Request & Error Rate",
Type: "graph",
Targets: []Target{
{
Expr: "sum without (instance,api)(rate(s3_requests_total[10m]))",
LegendFormat: "S3 Requests",
},
{
Expr: "sum without (instance,api)(rate(s3_errors_total[10m]))",
LegendFormat: "S3 Errors",
},
},
},
{
Title: "Total Open FDs",
Type: "stat",
Options: MetricOptions{
ReduceOptions: ReduceOptions{
Calcs: []string{
"mean",
},
},
},
Targets: []Target{
{
Expr: "sum without (instance)(process_open_fds)",
LegendFormat: "",
},
},
},
{
Title: "Total S3 Traffic Outbound",
Type: "stat",
Options: MetricOptions{
ReduceOptions: ReduceOptions{
Calcs: []string{
"mean",
},
},
},
Targets: []Target{
{
Expr: "sum without (instance)(s3_tx_bytes_total)",
LegendFormat: "",
},
},
},
{
Title: "Number of Objects",
Type: "stat",
Options: MetricOptions{
ReduceOptions: ReduceOptions{
Calcs: []string{
"lastNotNull",
},
},
},
Targets: []Target{
{
Expr: "topk(1, sum(bucket_objects_count) by (instance))",
LegendFormat: "",
},
},
},
{
Title: "Total Goroutines",
Type: "stat",
Options: MetricOptions{
ReduceOptions: ReduceOptions{
Calcs: []string{
"mean",
},
},
},
Targets: []Target{
{
Expr: "sum without (instance) (go_goroutines)",
LegendFormat: "",
},
},
},
{
Title: "S3 API Data Transfer",
Type: "graph",
Targets: []Target{
{
Expr: "sum without (instance,api)(rate(s3_tx_bytes_total[5m]))",
LegendFormat: "S3 Data Sent",
},
{
Expr: "sum without (instance,api)(rate(s3_rx_bytes_total[5m]))",
LegendFormat: "S3 Data Received",
},
},
},
{
Title: "Total S3 API Data Transfer",
Type: "graph",
Targets: []Target{
{
Expr: "sum without (instance) (s3_rx_bytes_total)",
LegendFormat: "S3 Bytes Received {{instance}}",
},
{
Expr: "sum without (instance) (s3_tx_bytes_total)",
LegendFormat: "S3 Bytes Sent {{instance}}",
},
},
},
{
Title: "Active S3 Requests",
Type: "graph",
Targets: []Target{
{
Expr: "s3_requests_current{instance=~\"$instance\"}",
LegendFormat: "Instance {{instance}} function {{api}}",
},
},
},
{
Title: "Internode Data Transfer",
Type: "graph",
Targets: []Target{
{
Expr: "internode_rx_bytes_total{instance=~\"$instance\"}",
LegendFormat: "Internode Bytes Received {{instance}}",
},
{
Expr: "internode_tx_bytes_total{instance=~\"$instance\"}",
LegendFormat: "Internode Bytes Sent {{instance}}",
},
},
},
{
Title: "Online Disks",
Type: "graph",
Targets: []Target{
{
Expr: "minio_disks_total{instance=~\"$instance\"} - minio_disks_offline{instance=~\"$instance\"}",
LegendFormat: "Online Disks {{instance}}",
},
},
},
{
Title: "Disk Usage",
Type: "graph",
Targets: []Target{
{
Expr: "disk_storage_used{disk=~\"$disk\",instance=~\"$instance\"}",
LegendFormat: "Used Capacity {{instance}} {{disk}}",
},
},
},
}
type Widget struct {
Title string
Type string
}
type DataResult struct {
Metric map[string]string `json:"metric"`
Values []interface{} `json:"values"`
}
type PromRespData struct {
ResultType string `json:"resultType"`
Result []DataResult `json:"result"`
}
type PromResp struct {
Status string `json:"status"`
Data PromRespData `json:"data"`
}
type LabelResponse struct {
Status string `json:"status"`
Data []string `json:"data"`
}
type LabelResults struct {
Label string
Response LabelResponse
}
// getAdminInfoResponse returns the response containing total buckets, objects and usage.
func getAdminInfoResponse(session *models.Principal) (*models.AdminInfoResponse, *models.Error) {
func getAdminInfoResponse(session *models.Principal, params admin_api.AdminInfoParams) (*models.AdminInfoResponse, *models.Error) {
prometheusURL := getPrometheusURL()
if prometheusURL == "" {
mAdmin, err := newMAdminClient(session)
if err != nil {
return nil, prepareError(err)
@@ -93,3 +480,202 @@ func getAdminInfoResponse(session *models.Principal) (*models.AdminInfoResponse,
}
return sessionResp, nil
}
labelResultsCh := make(chan LabelResults)
for _, lbl := range labels {
go func(lbl WidgetLabel) {
endpoint := fmt.Sprintf("%s/api/v1/label/%s/values", prometheusURL, lbl.Name)
resp, err := http.Get(endpoint)
if err != nil {
log.Println(err)
return
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Println(err)
}
}()
if resp.StatusCode != 200 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err)
return
}
log.Println(endpoint)
log.Println(resp.StatusCode)
log.Println(string(body))
return
}
var response LabelResponse
jd := json.NewDecoder(resp.Body)
if err = jd.Decode(&response); err != nil {
log.Println(err)
return
}
labelResultsCh <- LabelResults{Label: lbl.Name, Response: response}
}(lbl)
}
labelMap := make(map[string][]string)
// wait for as many goroutines that come back in less than 1 second
LabelsWaitLoop:
for {
select {
case <-time.After(1 * time.Second):
break LabelsWaitLoop
case res := <-labelResultsCh:
labelMap[res.Label] = res.Response.Data
if len(labelMap) >= len(labels) {
break LabelsWaitLoop
}
}
}
// launch a goroutines per widget
results := make(chan models.Widget)
for _, m := range widgets {
go func(m Metric, params admin_api.AdminInfoParams) {
targetResults := make(chan *models.ResultTarget)
// for each target we will launch another goroutine to fetch the values
for _, target := range m.Targets {
go func(target Target, params admin_api.AdminInfoParams) {
apiType := "query_range"
now := time.Now()
extraParamters := fmt.Sprintf("&start=%d&end=%d&step=%d", now.Add(-15*time.Minute).Unix(), now.Unix(), *params.Step)
if params.Start != nil && params.End != nil {
extraParamters = fmt.Sprintf("&start=%d&end=%d&step=%d", *params.Start, *params.End, *params.Step)
}
queryExpr := target.Expr
if strings.Contains(queryExpr, "$") {
var re = regexp.MustCompile(`\$([a-z]+)`)
for _, match := range re.FindAllStringSubmatch(queryExpr, -1) {
if val, ok := labelMap[match[1]]; ok {
queryExpr = strings.ReplaceAll(queryExpr, "$"+match[1], fmt.Sprintf("(%s)", strings.Join(val, "|")))
}
}
}
endpoint := fmt.Sprintf("%s/api/v1/%s?query=%s%s", getPrometheusURL(), apiType, url.QueryEscape(queryExpr), extraParamters)
resp, err := http.Get(endpoint)
if err != nil {
log.Println(err)
return
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Println(err)
}
}()
if resp.StatusCode != 200 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err)
return
}
log.Println(endpoint)
log.Println(resp.StatusCode)
log.Println(string(body))
return
}
var response PromResp
jd := json.NewDecoder(resp.Body)
if err = jd.Decode(&response); err != nil {
log.Println(err)
return
}
//body, _ := ioutil.ReadAll(resp.Body)
//err = json.Unmarshal(body, &response)
//if err != nil {
// log.Println(err)
//}
targetResult := models.ResultTarget{
LegendFormat: target.LegendFormat,
ResultType: response.Data.ResultType,
}
for _, r := range response.Data.Result {
targetResult.Result = append(targetResult.Result, &models.WidgetResult{
Metric: r.Metric,
Values: r.Values,
})
}
//xx, err := json.Marshal(response)
//if err != nil {
// log.Println(err)
//}
//log.Println("----", m.Title)
//log.Println(string(body))
//log.Println(string(xx))
//log.Println("=====")
targetResults <- &targetResult
}(target, params)
}
wdgtResult := models.Widget{
Title: m.Title,
Type: m.Type,
}
if len(m.Options.ReduceOptions.Calcs) > 0 {
wdgtResult.Options = &models.WidgetOptions{
ReduceOptions: &models.WidgetOptionsReduceOptions{
Calcs: m.Options.ReduceOptions.Calcs,
},
}
}
// count how many targets we have received
targetsReceived := 0
for res := range targetResults {
wdgtResult.Targets = append(wdgtResult.Targets, res)
targetsReceived++
// upon receiving the total number of targets needed, we can close the channel to not lock the goroutine
if targetsReceived >= len(m.Targets) {
close(targetResults)
}
}
results <- wdgtResult
}(m, params)
}
// count the number of widgets that have completed calculating
totalWidgets := 0
sessionResp := &models.AdminInfoResponse{}
var wdgts []*models.Widget
// wait for as many goroutines that come back in less than 1 second
WaitLoop:
for {
select {
case <-time.After(1 * time.Second):
break WaitLoop
case res := <-results:
wdgts = append(wdgts, &res)
totalWidgets++
if totalWidgets >= len(widgets) {
break WaitLoop
}
}
}
sessionResp.Widgets = wdgts
return sessionResp, nil
}

View File

@@ -30,6 +30,7 @@ import (
"strings"
"time"
"gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/types"
@@ -310,7 +311,7 @@ func getTenantInfo(tenant *operator.Tenant) *models.Tenant {
}
var deletion string
if tenant.ObjectMeta.DeletionTimestamp != nil {
deletion = tenant.ObjectMeta.DeletionTimestamp.String()
deletion = tenant.ObjectMeta.DeletionTimestamp.Format(time.RFC3339)
}
if tenant.HasConsoleEnabled() {
@@ -318,7 +319,7 @@ func getTenantInfo(tenant *operator.Tenant) *models.Tenant {
}
return &models.Tenant{
CreationDate: tenant.ObjectMeta.CreationTimestamp.String(),
CreationDate: tenant.ObjectMeta.CreationTimestamp.Format(time.RFC3339),
DeletionDate: deletion,
Name: tenant.Name,
TotalSize: totalSize,
@@ -351,14 +352,16 @@ func getTenantInfoResponse(session *models.Principal, params admin_api.TenantInf
info := getTenantInfo(minTenant)
if minTenant.Spec.Console != nil {
clientSet, err := cluster.K8sClient(session.STSSessionToken)
k8sClient := k8sClient{
client: clientSet,
}
if err != nil {
return nil, prepareError(err)
}
k8sClient := k8sClient{
client: clientSet,
}
if minTenant.Spec.Console != nil {
// obtain current subnet license for tenant (if exists)
license, _ := getSubscriptionLicense(context.Background(), &k8sClient, params.Namespace, minTenant.Spec.Console.ConsoleSecret.Name)
if license != "" {
@@ -373,6 +376,40 @@ func getTenantInfoResponse(session *models.Principal, params admin_api.TenantInf
}
}
// get tenant service
minTenant.EnsureDefaults()
//minio service
minSvc, err := k8sClient.getService(ctx, minTenant.Namespace, minTenant.MinIOCIServiceName(), metav1.GetOptions{})
if err != nil {
return nil, prepareError(err)
}
//console service
conSvc, err := k8sClient.getService(ctx, minTenant.Namespace, minTenant.ConsoleCIServiceName(), metav1.GetOptions{})
if err != nil {
return nil, prepareError(err)
}
schema := "http"
consolePort := ":9090"
if minTenant.TLS() {
schema = "https"
consolePort = ":9443"
}
var minioEndpoint string
var consoleEndpoint string
if len(minSvc.Status.LoadBalancer.Ingress) > 0 {
minioEndpoint = fmt.Sprintf("%s://%s", schema, minSvc.Status.LoadBalancer.Ingress[0].IP)
}
if len(conSvc.Status.LoadBalancer.Ingress) > 0 {
consoleEndpoint = fmt.Sprintf("%s://%s%s", schema, conSvc.Status.LoadBalancer.Ingress[0].IP, consolePort)
}
if minioEndpoint != "" || consoleEndpoint != "" {
info.Endpoints = &models.TenantEndpoints{
Console: consoleEndpoint,
Minio: minioEndpoint,
}
}
return info, nil
}
@@ -407,11 +444,11 @@ func listTenants(ctx context.Context, operatorClient OperatorClientI, namespace
var deletion string
if tenant.ObjectMeta.DeletionTimestamp != nil {
deletion = tenant.ObjectMeta.DeletionTimestamp.String()
deletion = tenant.ObjectMeta.DeletionTimestamp.Format(time.RFC3339)
}
tenants = append(tenants, &models.TenantList{
CreationDate: tenant.ObjectMeta.CreationTimestamp.String(),
CreationDate: tenant.ObjectMeta.CreationTimestamp.Format(time.RFC3339),
DeletionDate: deletion,
Name: tenant.ObjectMeta.Name,
PoolCount: int64(len(tenant.Spec.Pools)),
@@ -702,7 +739,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
minInst.Spec.Console = &operator.ConsoleConfiguration{
Replicas: 1,
Image: ConsoleImageVersion,
Image: getConsoleImage(),
ConsoleSecret: &corev1.LocalObjectReference{Name: consoleSecretName},
Resources: corev1.ResourceRequirements{
Requests: map[corev1.ResourceName]resource.Quantity{
@@ -808,6 +845,26 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
if tenantReq.ConsoleImage != "" {
minInst.Spec.Console.Image = tenantReq.ConsoleImage
}
// default activate lgo search and prometheus
minInst.Spec.Log = &operator.LogConfig{
Image: "miniodev/logsearch:v4.0.0",
Audit: &operator.AuditConfig{DiskCapacityGB: swag.Int(10)},
}
minInst.Spec.Prometheus = &operator.PrometheusConfig{
DiskCapacityDB: swag.Int(5),
}
// expose services
if tenantReq.ExposeMinio || tenantReq.ExposeConsole {
minInst.Spec.ExposeServices = &operator.ExposeServices{
MinIO: tenantReq.ExposeMinio,
Console: tenantReq.ExposeConsole,
}
log.Println("happened")
}
yo, _ := yaml.Marshal(minInst)
log.Println(string(yo))
opClient, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {

View File

@@ -26,6 +26,7 @@ import (
"net/http"
"reflect"
"testing"
"time"
"github.com/go-openapi/swag"
"github.com/minio/console/cluster"
@@ -325,7 +326,7 @@ func Test_TenantInfo(t *testing.T) {
},
},
want: &models.Tenant{
CreationDate: testTimeStamp.String(),
CreationDate: testTimeStamp.Format(time.RFC3339),
Name: "tenant1",
TotalSize: int64(8388608),
CurrentState: "ready",
@@ -389,8 +390,8 @@ func Test_TenantInfo(t *testing.T) {
},
},
want: &models.Tenant{
CreationDate: testTimeStamp.String(),
DeletionDate: testTimeStamp.String(),
CreationDate: testTimeStamp.Format(time.RFC3339),
DeletionDate: testTimeStamp.Format(time.RFC3339),
Name: "tenant1",
TotalSize: int64(8388608),
CurrentState: "ready",
@@ -435,7 +436,7 @@ func Test_TenantInfo(t *testing.T) {
},
},
want: &models.Tenant{
CreationDate: testTimeStamp.String(),
CreationDate: testTimeStamp.Format(time.RFC3339),
Name: "tenant1",
CurrentState: "ready",
Namespace: "minio-ns",
@@ -470,7 +471,7 @@ func Test_TenantInfo(t *testing.T) {
},
},
want: &models.Tenant{
CreationDate: testTimeStamp.String(),
CreationDate: testTimeStamp.Format(time.RFC3339),
Name: "tenant1",
CurrentState: "ready",
Namespace: "minio-ns",

View File

@@ -23,6 +23,7 @@ import (
"log"
"net/http"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/minio/minio/pkg/madmin"
@@ -92,7 +93,7 @@ func shortTrace(info *madmin.ServiceTraceInfo) shortTraceMsg {
t := info.Trace
s := shortTraceMsg{}
s.Time = t.ReqInfo.Time.String()
s.Time = t.ReqInfo.Time.Format(time.RFC3339)
s.Path = t.ReqInfo.Path
s.Query = t.ReqInfo.RawQuery
s.FuncName = t.FuncName

View File

@@ -19,8 +19,10 @@ package restapi
import (
"crypto/x509"
"fmt"
"io/ioutil"
"strconv"
"strings"
"sync"
"time"
"github.com/minio/minio/pkg/certs"
@@ -44,6 +46,13 @@ var TLSRedirect = "off"
var SessionDuration = 45 * time.Minute
var logSearchAPI string
var logSearchURL string
var prometheusURL string
var consoleImage string
var once sync.Once
func getMinIOServer() string {
return strings.TrimSpace(env.Get(ConsoleMinIOServer, "http://localhost:9000"))
}
@@ -220,11 +229,39 @@ func getSecureExpectCTHeader() string {
return env.Get(ConsoleSecureExpectCTHeader, "")
}
func getLogSearchAPIToken() string {
once.Do(func() {
initVars()
})
return logSearchAPI
}
func getLogSearchURL() string {
once.Do(func() {
initVars()
})
return logSearchURL
}
func getPrometheusURL() string {
once.Do(func() {
initVars()
})
return prometheusURL
}
// GetSubnetLicense returns the current subnet jwt license
func GetSubnetLicense() string {
return env.Get(ConsoleSubnetLicense, "")
}
func initVars() {
logSearchAPI = env.Get(LogSearchQueryAuthToken, "")
logSearchURL = env.Get(LogSearchURL, "http://localhost:8080")
prometheusURL = env.Get(PrometheusURL, "")
consoleImage = env.Get(ConsoleOperatorConsoleImage, ConsoleImageDefaultVersion)
}
var (
// GlobalRootCAs is CA root certificates, a nil value means system certs pool will be used
GlobalRootCAs *x509.CertPool
@@ -233,3 +270,20 @@ var (
// GlobalTLSCertsManager custom TLS Manager for SNI support
GlobalTLSCertsManager *certs.Manager
)
// getK8sSAToken assumes the plugin is running inside a k8s pod and extract the current service account from the
// /var/run/secrets/kubernetes.io/serviceaccount/token file
func getK8sSAToken() string {
dat, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
return env.Get(ConsoleOperatorSAToken, "")
}
return string(dat)
}
func getConsoleImage() string {
once.Do(func() {
initVars()
})
return consoleImage
}

View File

@@ -121,6 +121,8 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler {
registerServiceAccountsHandlers(api)
// Register admin remote buckets
registerAdminBucketRemoteHandlers(api)
// Register admin log search
registerLogSearchHandlers(api)
// Register admin subscription handlers
registerSubscriptionHandlers(api)

View File

@@ -48,6 +48,11 @@ const (
ConsoleSecureReferrerPolicy = "CONSOLE_SECURE_REFERRER_POLICY"
ConsoleSecureFeaturePolicy = "CONSOLE_SECURE_FEATURE_POLICY"
ConsoleSecureExpectCTHeader = "CONSOLE_SECURE_EXPECT_CT_HEADER"
ConsoleOperatorSAToken = "CONSOLE_OPERATOR_SA_TOKEN"
ConsoleOperatorConsoleImage = "CONSOLE_OPERATOR_CONSOLE_IMAGE"
LogSearchURL = "CONSOLE_LOG_QUERY_URL"
PrometheusURL = "CONSOLE_PROMETHEUS_URL"
LogSearchQueryAuthToken = "LOGSEARCH_QUERY_AUTH_TOKEN"
// Constants for prometheus annotations
prometheusPath = "prometheus.io/path"
@@ -57,8 +62,8 @@ const (
// Image versions
const (
KESImageVersion = "minio/kes:v0.12.1"
ConsoleImageVersion = "minio/console:v0.4.6"
KESImageVersion = "minio/kes:v0.13.1"
ConsoleImageDefaultVersion = "minio/console:v0.4.6"
)
// K8s

View File

@@ -115,6 +115,24 @@ func init() {
],
"summary": "Returns information about the deployment",
"operationId": "AdminInfo",
"parameters": [
{
"type": "integer",
"name": "start",
"in": "query"
},
{
"type": "integer",
"name": "end",
"in": "query"
},
{
"type": "integer",
"default": 15,
"name": "step",
"in": "query"
}
],
"responses": {
"200": {
"description": "A successful response.",
@@ -1713,6 +1731,70 @@ func init() {
}
}
},
"/logs/search": {
"get": {
"tags": [
"UserAPI"
],
"summary": "Search the logs",
"operationId": "LogSearch",
"parameters": [
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "Filter Parameters",
"name": "fp",
"in": "query"
},
{
"type": "number",
"format": "int32",
"default": 10,
"name": "pageSize",
"in": "query"
},
{
"type": "number",
"format": "int32",
"default": 0,
"name": "pageNo",
"in": "query"
},
{
"enum": [
"timeDesc",
"timeAsc"
],
"type": "string",
"default": "timeDesc",
"name": "order",
"in": "query"
},
{
"type": "string",
"name": "timeStart",
"in": "query"
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/logSearchResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/namespaces/{namespace}/resourcequotas/{resource-quota-name}": {
"get": {
"tags": [
@@ -3119,6 +3201,12 @@ func init() {
},
"usage": {
"type": "integer"
},
"widgets": {
"type": "array",
"items": {
"$ref": "#/definitions/widget"
}
}
}
},
@@ -3517,6 +3605,12 @@ func init() {
"erasureCodingParity": {
"type": "integer"
},
"expose_console": {
"type": "boolean"
},
"expose_minio": {
"type": "boolean"
},
"idp": {
"type": "object",
"$ref": "#/definitions/idpConfiguration"
@@ -3985,6 +4079,15 @@ func init() {
}
}
},
"logSearchResponse": {
"type": "object",
"properties": {
"results": {
"type": "object",
"title": "list of log search responses"
}
}
},
"loginDetails": {
"type": "object",
"properties": {
@@ -4838,6 +4941,23 @@ func init() {
}
}
},
"resultTarget": {
"type": "object",
"properties": {
"legendFormat": {
"type": "string"
},
"result": {
"type": "array",
"items": {
"$ref": "#/definitions/widgetResult"
}
},
"resultType": {
"type": "string"
}
}
},
"serviceAccountCreds": {
"type": "object",
"properties": {
@@ -5071,6 +5191,17 @@ func init() {
"enable_prometheus": {
"type": "boolean"
},
"endpoints": {
"type": "object",
"properties": {
"console": {
"type": "string"
},
"minio": {
"type": "string"
}
}
},
"image": {
"type": "string"
},
@@ -5312,6 +5443,54 @@ func init() {
}
}
}
},
"widget": {
"type": "object",
"properties": {
"options": {
"type": "object",
"properties": {
"reduceOptions": {
"type": "object",
"properties": {
"calcs": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"targets": {
"type": "array",
"items": {
"$ref": "#/definitions/resultTarget"
}
},
"title": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"widgetResult": {
"type": "object",
"properties": {
"metric": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"values": {
"type": "array",
"items": {}
}
}
}
},
"securityDefinitions": {
@@ -5409,6 +5588,24 @@ func init() {
],
"summary": "Returns information about the deployment",
"operationId": "AdminInfo",
"parameters": [
{
"type": "integer",
"name": "start",
"in": "query"
},
{
"type": "integer",
"name": "end",
"in": "query"
},
{
"type": "integer",
"default": 15,
"name": "step",
"in": "query"
}
],
"responses": {
"200": {
"description": "A successful response.",
@@ -7007,6 +7204,70 @@ func init() {
}
}
},
"/logs/search": {
"get": {
"tags": [
"UserAPI"
],
"summary": "Search the logs",
"operationId": "LogSearch",
"parameters": [
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "Filter Parameters",
"name": "fp",
"in": "query"
},
{
"type": "number",
"format": "int32",
"default": 10,
"name": "pageSize",
"in": "query"
},
{
"type": "number",
"format": "int32",
"default": 0,
"name": "pageNo",
"in": "query"
},
{
"enum": [
"timeDesc",
"timeAsc"
],
"type": "string",
"default": "timeDesc",
"name": "order",
"in": "query"
},
{
"type": "string",
"name": "timeStart",
"in": "query"
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/logSearchResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/namespaces/{namespace}/resourcequotas/{resource-quota-name}": {
"get": {
"tags": [
@@ -8799,6 +9060,17 @@ func init() {
}
}
},
"TenantEndpoints": {
"type": "object",
"properties": {
"console": {
"type": "string"
},
"minio": {
"type": "string"
}
}
},
"VaultConfigurationApprole": {
"type": "object",
"required": [
@@ -8844,6 +9116,33 @@ func init() {
}
}
},
"WidgetOptions": {
"type": "object",
"properties": {
"reduceOptions": {
"type": "object",
"properties": {
"calcs": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"WidgetOptionsReduceOptions": {
"type": "object",
"properties": {
"calcs": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"accountChangePasswordRequest": {
"type": "object",
"required": [
@@ -8936,6 +9235,12 @@ func init() {
},
"usage": {
"type": "integer"
},
"widgets": {
"type": "array",
"items": {
"$ref": "#/definitions/widget"
}
}
}
},
@@ -9334,6 +9639,12 @@ func init() {
"erasureCodingParity": {
"type": "integer"
},
"expose_console": {
"type": "boolean"
},
"expose_minio": {
"type": "boolean"
},
"idp": {
"type": "object",
"$ref": "#/definitions/idpConfiguration"
@@ -9802,6 +10113,15 @@ func init() {
}
}
},
"logSearchResponse": {
"type": "object",
"properties": {
"results": {
"type": "object",
"title": "list of log search responses"
}
}
},
"loginDetails": {
"type": "object",
"properties": {
@@ -10520,6 +10840,23 @@ func init() {
}
}
},
"resultTarget": {
"type": "object",
"properties": {
"legendFormat": {
"type": "string"
},
"result": {
"type": "array",
"items": {
"$ref": "#/definitions/widgetResult"
}
},
"resultType": {
"type": "string"
}
}
},
"serviceAccountCreds": {
"type": "object",
"properties": {
@@ -10753,6 +11090,17 @@ func init() {
"enable_prometheus": {
"type": "boolean"
},
"endpoints": {
"type": "object",
"properties": {
"console": {
"type": "string"
},
"minio": {
"type": "string"
}
}
},
"image": {
"type": "string"
},
@@ -10994,6 +11342,54 @@ func init() {
}
}
}
},
"widget": {
"type": "object",
"properties": {
"options": {
"type": "object",
"properties": {
"reduceOptions": {
"type": "object",
"properties": {
"calcs": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"targets": {
"type": "array",
"items": {
"$ref": "#/definitions/resultTarget"
}
},
"title": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"widgetResult": {
"type": "object",
"properties": {
"metric": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"values": {
"type": "array",
"items": {}
}
}
}
},
"securityDefinitions": {

View File

@@ -26,14 +26,25 @@ import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// NewAdminInfoParams creates a new AdminInfoParams object
// no default values defined in spec.
// with the default values initialized.
func NewAdminInfoParams() AdminInfoParams {
return AdminInfoParams{}
var (
// initialize parameters with default values
stepDefault = int64(15)
)
return AdminInfoParams{
Step: &stepDefault,
}
}
// AdminInfoParams contains all the bound params for the admin info operation
@@ -44,6 +55,20 @@ type AdminInfoParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
In: query
*/
End *int64
/*
In: query
*/
Start *int64
/*
In: query
Default: 15
*/
Step *int64
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
@@ -55,8 +80,92 @@ func (o *AdminInfoParams) BindRequest(r *http.Request, route *middleware.Matched
o.HTTPRequest = r
qs := runtime.Values(r.URL.Query())
qEnd, qhkEnd, _ := qs.GetOK("end")
if err := o.bindEnd(qEnd, qhkEnd, route.Formats); err != nil {
res = append(res, err)
}
qStart, qhkStart, _ := qs.GetOK("start")
if err := o.bindStart(qStart, qhkStart, route.Formats); err != nil {
res = append(res, err)
}
qStep, qhkStep, _ := qs.GetOK("step")
if err := o.bindStep(qStep, qhkStep, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindEnd binds and validates parameter End from query.
func (o *AdminInfoParams) bindEnd(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
return nil
}
value, err := swag.ConvertInt64(raw)
if err != nil {
return errors.InvalidType("end", "query", "int64", raw)
}
o.End = &value
return nil
}
// bindStart binds and validates parameter Start from query.
func (o *AdminInfoParams) bindStart(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
return nil
}
value, err := swag.ConvertInt64(raw)
if err != nil {
return errors.InvalidType("start", "query", "int64", raw)
}
o.Start = &value
return nil
}
// bindStep binds and validates parameter Step from query.
func (o *AdminInfoParams) bindStep(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
// Default values have been previously initialized by NewAdminInfoParams()
return nil
}
value, err := swag.ConvertInt64(raw)
if err != nil {
return errors.InvalidType("step", "query", "int64", raw)
}
o.Step = &value
return nil
}

View File

@@ -26,11 +26,19 @@ import (
"errors"
"net/url"
golangswaggerpaths "path"
"github.com/go-openapi/swag"
)
// AdminInfoURL generates an URL for the admin info operation
type AdminInfoURL struct {
End *int64
Start *int64
Step *int64
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
@@ -60,6 +68,34 @@ func (o *AdminInfoURL) Build() (*url.URL, error) {
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
qs := make(url.Values)
var endQ string
if o.End != nil {
endQ = swag.FormatInt64(*o.End)
}
if endQ != "" {
qs.Set("end", endQ)
}
var startQ string
if o.Start != nil {
startQ = swag.FormatInt64(*o.Start)
}
if startQ != "" {
qs.Set("start", startQ)
}
var stepQ string
if o.Step != nil {
stepQ = swag.FormatInt64(*o.Step)
}
if stepQ != "" {
qs.Set("step", stepQ)
}
_result.RawQuery = qs.Encode()
return &_result, nil
}

View File

@@ -208,6 +208,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
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")
}),
UserAPILogSearchHandler: user_api.LogSearchHandlerFunc(func(params user_api.LogSearchParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.LogSearch has not yet been implemented")
}),
UserAPILoginHandler: user_api.LoginHandlerFunc(func(params user_api.LoginParams) middleware.Responder {
return middleware.NotImplemented("operation user_api.Login has not yet been implemented")
}),
@@ -473,6 +476,8 @@ type ConsoleAPI struct {
UserAPIListUserServiceAccountsHandler user_api.ListUserServiceAccountsHandler
// AdminAPIListUsersHandler sets the operation handler for the list users operation
AdminAPIListUsersHandler admin_api.ListUsersHandler
// UserAPILogSearchHandler sets the operation handler for the log search operation
UserAPILogSearchHandler user_api.LogSearchHandler
// UserAPILoginHandler sets the operation handler for the login operation
UserAPILoginHandler user_api.LoginHandler
// UserAPILoginDetailHandler sets the operation handler for the login detail operation
@@ -771,6 +776,9 @@ func (o *ConsoleAPI) Validate() error {
if o.AdminAPIListUsersHandler == nil {
unregistered = append(unregistered, "admin_api.ListUsersHandler")
}
if o.UserAPILogSearchHandler == nil {
unregistered = append(unregistered, "user_api.LogSearchHandler")
}
if o.UserAPILoginHandler == nil {
unregistered = append(unregistered, "user_api.LoginHandler")
}
@@ -1182,6 +1190,10 @@ func (o *ConsoleAPI) initHandlerCache() {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/users"] = admin_api.NewListUsers(o.context, o.AdminAPIListUsersHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/logs/search"] = user_api.NewLogSearch(o.context, o.UserAPILogSearchHandler)
if o.handlers["POST"] == nil {
o.handlers["POST"] = make(map[string]http.Handler)
}

View 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/console/models"
)
// LogSearchHandlerFunc turns a function with the right signature into a log search handler
type LogSearchHandlerFunc func(LogSearchParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn LogSearchHandlerFunc) Handle(params LogSearchParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// LogSearchHandler interface for that can handle valid log search params
type LogSearchHandler interface {
Handle(LogSearchParams, *models.Principal) middleware.Responder
}
// NewLogSearch creates a new http.Handler for the log search operation
func NewLogSearch(ctx *middleware.Context, handler LogSearchHandler) *LogSearch {
return &LogSearch{Context: ctx, Handler: handler}
}
/*LogSearch swagger:route GET /logs/search UserAPI logSearch
Search the logs
*/
type LogSearch struct {
Context *middleware.Context
Handler LogSearchHandler
}
func (o *LogSearch) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
r = rCtx
}
var Params = NewLogSearchParams()
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)
}

View File

@@ -0,0 +1,253 @@
// 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"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// NewLogSearchParams creates a new LogSearchParams object
// with the default values initialized.
func NewLogSearchParams() LogSearchParams {
var (
// initialize parameters with default values
orderDefault = string("timeDesc")
pageNoDefault = int32(0)
pageSizeDefault = int32(10)
)
return LogSearchParams{
Order: &orderDefault,
PageNo: &pageNoDefault,
PageSize: &pageSizeDefault,
}
}
// LogSearchParams contains all the bound params for the log search operation
// typically these are obtained from a http.Request
//
// swagger:parameters LogSearch
type LogSearchParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*Filter Parameters
In: query
Collection Format: multi
*/
Fp []string
/*
In: query
Default: "timeDesc"
*/
Order *string
/*
In: query
Default: 0
*/
PageNo *int32
/*
In: query
Default: 10
*/
PageSize *int32
/*
In: query
*/
TimeStart *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 NewLogSearchParams() beforehand.
func (o *LogSearchParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
qs := runtime.Values(r.URL.Query())
qFp, qhkFp, _ := qs.GetOK("fp")
if err := o.bindFp(qFp, qhkFp, route.Formats); err != nil {
res = append(res, err)
}
qOrder, qhkOrder, _ := qs.GetOK("order")
if err := o.bindOrder(qOrder, qhkOrder, route.Formats); err != nil {
res = append(res, err)
}
qPageNo, qhkPageNo, _ := qs.GetOK("pageNo")
if err := o.bindPageNo(qPageNo, qhkPageNo, route.Formats); err != nil {
res = append(res, err)
}
qPageSize, qhkPageSize, _ := qs.GetOK("pageSize")
if err := o.bindPageSize(qPageSize, qhkPageSize, route.Formats); err != nil {
res = append(res, err)
}
qTimeStart, qhkTimeStart, _ := qs.GetOK("timeStart")
if err := o.bindTimeStart(qTimeStart, qhkTimeStart, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindFp binds and validates array parameter Fp from query.
//
// Arrays are parsed according to CollectionFormat: "multi" (defaults to "csv" when empty).
func (o *LogSearchParams) bindFp(rawData []string, hasKey bool, formats strfmt.Registry) error {
// CollectionFormat: multi
fpIC := rawData
if len(fpIC) == 0 {
return nil
}
var fpIR []string
for _, fpIV := range fpIC {
fpI := fpIV
fpIR = append(fpIR, fpI)
}
o.Fp = fpIR
return nil
}
// bindOrder binds and validates parameter Order from query.
func (o *LogSearchParams) bindOrder(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
// Default values have been previously initialized by NewLogSearchParams()
return nil
}
o.Order = &raw
if err := o.validateOrder(formats); err != nil {
return err
}
return nil
}
// validateOrder carries on validations for parameter Order
func (o *LogSearchParams) validateOrder(formats strfmt.Registry) error {
if err := validate.EnumCase("order", "query", *o.Order, []interface{}{"timeDesc", "timeAsc"}, true); err != nil {
return err
}
return nil
}
// bindPageNo binds and validates parameter PageNo from query.
func (o *LogSearchParams) bindPageNo(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
// Default values have been previously initialized by NewLogSearchParams()
return nil
}
value, err := swag.ConvertInt32(raw)
if err != nil {
return errors.InvalidType("pageNo", "query", "int32", raw)
}
o.PageNo = &value
return nil
}
// bindPageSize binds and validates parameter PageSize from query.
func (o *LogSearchParams) bindPageSize(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
// Default values have been previously initialized by NewLogSearchParams()
return nil
}
value, err := swag.ConvertInt32(raw)
if err != nil {
return errors.InvalidType("pageSize", "query", "int32", raw)
}
o.PageSize = &value
return nil
}
// bindTimeStart binds and validates parameter TimeStart from query.
func (o *LogSearchParams) bindTimeStart(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
return nil
}
o.TimeStart = &raw
return nil
}

View File

@@ -0,0 +1,133 @@
// 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/runtime"
"github.com/minio/console/models"
)
// LogSearchOKCode is the HTTP code returned for type LogSearchOK
const LogSearchOKCode int = 200
/*LogSearchOK A successful response.
swagger:response logSearchOK
*/
type LogSearchOK struct {
/*
In: Body
*/
Payload *models.LogSearchResponse `json:"body,omitempty"`
}
// NewLogSearchOK creates LogSearchOK with default headers values
func NewLogSearchOK() *LogSearchOK {
return &LogSearchOK{}
}
// WithPayload adds the payload to the log search o k response
func (o *LogSearchOK) WithPayload(payload *models.LogSearchResponse) *LogSearchOK {
o.Payload = payload
return o
}
// SetPayload sets the payload to the log search o k response
func (o *LogSearchOK) SetPayload(payload *models.LogSearchResponse) {
o.Payload = payload
}
// WriteResponse to the client
func (o *LogSearchOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(200)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}
/*LogSearchDefault Generic error response.
swagger:response logSearchDefault
*/
type LogSearchDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewLogSearchDefault creates LogSearchDefault with default headers values
func NewLogSearchDefault(code int) *LogSearchDefault {
if code <= 0 {
code = 500
}
return &LogSearchDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the log search default response
func (o *LogSearchDefault) WithStatusCode(code int) *LogSearchDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the log search default response
func (o *LogSearchDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the log search default response
func (o *LogSearchDefault) WithPayload(payload *models.Error) *LogSearchDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the log search default response
func (o *LogSearchDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *LogSearchDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -0,0 +1,164 @@
// 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 (
"errors"
"net/url"
golangswaggerpaths "path"
"github.com/go-openapi/swag"
)
// LogSearchURL generates an URL for the log search operation
type LogSearchURL struct {
Fp []string
Order *string
PageNo *int32
PageSize *int32
TimeStart *string
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *LogSearchURL) WithBasePath(bp string) *LogSearchURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *LogSearchURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *LogSearchURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/logs/search"
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
qs := make(url.Values)
var fpIR []string
for _, fpI := range o.Fp {
fpIS := fpI
if fpIS != "" {
fpIR = append(fpIR, fpIS)
}
}
fp := swag.JoinByFormat(fpIR, "multi")
for _, qsv := range fp {
qs.Add("fp", qsv)
}
var orderQ string
if o.Order != nil {
orderQ = *o.Order
}
if orderQ != "" {
qs.Set("order", orderQ)
}
var pageNoQ string
if o.PageNo != nil {
pageNoQ = swag.FormatInt32(*o.PageNo)
}
if pageNoQ != "" {
qs.Set("pageNo", pageNoQ)
}
var pageSizeQ string
if o.PageSize != nil {
pageSizeQ = swag.FormatInt32(*o.PageSize)
}
if pageSizeQ != "" {
qs.Set("pageSize", pageSizeQ)
}
var timeStartQ string
if o.TimeStart != nil {
timeStartQ = *o.TimeStart
}
if timeStartQ != "" {
qs.Set("timeStart", timeStartQ)
}
_result.RawQuery = qs.Encode()
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *LogSearchURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *LogSearchURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *LogSearchURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on LogSearchURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on LogSearchURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *LogSearchURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -272,7 +272,7 @@ func getAccountInfo(ctx context.Context, client MinioAdmin) ([]*models.Bucket, e
}
var bucketInfos []*models.Bucket
for _, bucket := range info.Buckets {
bucketElem := &models.Bucket{Name: swag.String(bucket.Name), CreationDate: bucket.Created.String(), Size: int64(bucket.Size)}
bucketElem := &models.Bucket{Name: swag.String(bucket.Name), CreationDate: bucket.Created.Format(time.RFC3339), Size: int64(bucket.Size)}
bucketInfos = append(bucketInfos, bucketElem)
}
return bucketInfos, nil

View File

@@ -128,7 +128,7 @@ func TestListBucket(t *testing.T) {
assert.Equal(len(mockBucketList.Buckets), len(bucketList), fmt.Sprintf("Failed on %s: length of bucket's lists is not the same", function))
for i, b := range bucketList {
assert.Equal(mockBucketList.Buckets[i].Name, *b.Name)
assert.Equal(mockBucketList.Buckets[i].Created.String(), b.CreationDate)
assert.Equal(mockBucketList.Buckets[i].Created.Format(time.RFC3339), b.CreationDate)
assert.Equal(mockBucketList.Buckets[i].Name, *b.Name)
assert.Equal(int64(mockBucketList.Buckets[i].Size), b.Size)
}

100
restapi/user_log_search.go Normal file
View File

@@ -0,0 +1,100 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package restapi
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/go-openapi/swag"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
"github.com/minio/console/restapi/operations"
"github.com/minio/console/restapi/operations/user_api"
logsearchServer "github.com/minio/operator/logsearchapi/server"
)
func registerLogSearchHandlers(api *operations.ConsoleAPI) {
// log search
api.UserAPILogSearchHandler = user_api.LogSearchHandlerFunc(func(params user_api.LogSearchParams, session *models.Principal) middleware.Responder {
searchResp, err := getLogSearchResponse(params)
if err != nil {
return user_api.NewLogSearchDefault(int(err.Code)).WithPayload(err)
}
return user_api.NewLogSearchOK().WithPayload(searchResp)
})
}
func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
}
// getLogSearchResponse performs a query to Log Search if Enabled
func getLogSearchResponse(params user_api.LogSearchParams) (*models.LogSearchResponse, *models.Error) {
token := getLogSearchAPIToken()
endpoint := fmt.Sprintf("%s/api/query?token=%s&q=reqinfo", getLogSearchURL(), token)
for _, fp := range params.Fp {
endpoint = fmt.Sprintf("%s&fp=%s", endpoint, fp)
}
endpoint = fmt.Sprintf("%s&%s=ok", endpoint, *params.Order)
// timeStart
if params.TimeStart != nil && *params.TimeStart != "" {
endpoint = fmt.Sprintf("%s&timeStart=%s", endpoint, *params.TimeStart)
}
// page size and page number
endpoint = fmt.Sprintf("%s&pageSize=%d", endpoint, *params.PageSize)
endpoint = fmt.Sprintf("%s&pageNo=%d", endpoint, *params.PageNo)
return logSearch(endpoint)
}
func logSearch(endpoint string) (*models.LogSearchResponse, *models.Error) {
resp, err := http.Get(endpoint)
if err != nil {
return nil, prepareError(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
log.Println("Error Status Code", resp.StatusCode)
_, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, prepareError(err)
}
return nil, &models.Error{
Code: 500,
Message: swag.String("Error retrieving logs"),
}
}
var results []logsearchServer.ReqInfoRow
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
return nil, prepareError(err)
}
response := models.LogSearchResponse{
Results: results,
}
return &response, nil
}

View File

@@ -0,0 +1,129 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package restapi
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"github.com/go-openapi/swag"
"github.com/minio/console/models"
logsearchServer "github.com/minio/operator/logsearchapi/server"
asrt "github.com/stretchr/testify/assert"
)
func TestLogSearch(t *testing.T) {
responseItem := []logsearchServer.ReqInfoRow{
{
Time: time.Time{},
APIName: "GetConfigKV",
Bucket: "",
Object: "",
TimeToResponseNs: 45254653,
RemoteHost: "10.116.1.94",
RequestID: "16595A4E30CCFE79",
UserAgent: "MinIO (linux; amd64) madmin-go/0.0.1",
ResponseStatus: "OK",
ResponseStatusCode: 200,
RequestContentLength: nil,
ResponseContentLength: nil,
}, {
Time: time.Time{},
APIName: "AssumeRole",
Bucket: "",
Object: "",
TimeToResponseNs: 307423794,
RemoteHost: "127.0.0.1",
RequestID: "16595A4DA906FBA9",
UserAgent: "Go-http-client/1.1",
ResponseStatus: "OK",
ResponseStatusCode: 200,
RequestContentLength: nil,
ResponseContentLength: nil,
},
}
assert := asrt.New(t)
type args struct {
apiResponse string
apiResponseCode int
}
response, _ := json.Marshal(responseItem)
successfulResponse := &models.LogSearchResponse{
Results: responseItem,
}
tests := []struct {
name string
args args
expectedResponse *models.LogSearchResponse
expectedError *models.Error
}{
{
name: "200 Success response",
args: args{
apiResponse: fmt.Sprintf("%s\n", response),
apiResponseCode: 200,
},
expectedResponse: successfulResponse,
expectedError: nil,
},
{
name: "500 unsuccessful response",
args: args{
apiResponse: "Some random error",
apiResponseCode: 500,
},
expectedResponse: nil,
expectedError: &models.Error{
Code: 500,
Message: swag.String("Error retrieving logs"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testRequest := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tt.args.apiResponseCode)
fmt.Fprintln(w, tt.args.apiResponse)
}))
defer testRequest.Close()
resp, err := logSearch(testRequest.URL)
if tt.expectedError != nil {
fmt.Println(t.Name())
assert.Equal(tt.expectedError.Code, err.Code, fmt.Sprintf("logSearch() error code: `%v`, wantErr: `%v`", err.Code, tt.expectedError))
assert.Equal(tt.expectedError.Message, err.Message, fmt.Sprintf("logSearch() error message: `%v`, wantErr: `%v`", err.Message, tt.expectedError))
} else {
assert.Nil(err, fmt.Sprintf("logSearch() error: %v, wantErr: %v", err, tt.expectedError))
if !reflect.DeepEqual(resp, tt.expectedResponse) {
t.Errorf("logSearch() resp: %v, expectedResponse: %v", resp, tt.expectedResponse)
return
}
}
})
}
}

View File

@@ -176,9 +176,8 @@ func getLoginDetailsResponse() (*models.LoginDetails, *models.Error) {
defer cancel()
loginStrategy := models.LoginDetailsLoginStrategyForm
redirectURL := ""
if acl.GetOperatorMode() {
loginStrategy = models.LoginDetailsLoginStrategyServiceAccount
} else if oauth2.IsIdpEnabled() {
if oauth2.IsIdpEnabled() {
loginStrategy = models.LoginDetailsLoginStrategyRedirect
// initialize new oauth2 client
oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil)
@@ -188,7 +187,10 @@ func getLoginDetailsResponse() (*models.LoginDetails, *models.Error) {
// Validate user against IDP
identityProvider := &auth.IdentityProvider{Client: oauth2Client}
redirectURL = identityProvider.GenerateLoginURL()
} else if acl.GetOperatorMode() {
loginStrategy = models.LoginDetailsLoginStrategyServiceAccount
}
loginDetails := &models.LoginDetails{
LoginStrategy: loginStrategy,
Redirect: redirectURL,
@@ -209,7 +211,22 @@ func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI,
func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
if oauth2.IsIdpEnabled() {
if acl.GetOperatorMode() {
creds, err := newConsoleCredentials("", getK8sSAToken(), "")
if err != nil {
return nil, prepareError(err)
}
credentials := consoleCredentials{consoleCredentials: creds, actions: []string{}}
token, err := login(credentials)
if err != nil {
return nil, prepareError(errInvalidCredentials, nil, err)
}
// serialize output
loginResponse := &models.LoginResponse{
SessionID: *token,
}
return loginResponse, nil
} else if oauth2.IsIdpEnabled() {
// initialize new oauth2 client
oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil)
if err != nil {

View File

@@ -173,7 +173,7 @@ func listBucketObjects(ctx context.Context, client MinioClient, bucketName strin
obj := &models.BucketObject{
Name: lsObj.Key,
Size: lsObj.Size,
LastModified: lsObj.LastModified.String(),
LastModified: lsObj.LastModified.Format(time.RFC3339),
ContentType: lsObj.ContentType,
VersionID: lsObj.VersionID,
IsLatest: lsObj.IsLatest,
@@ -205,7 +205,7 @@ func listBucketObjects(ctx context.Context, client MinioClient, bucketName strin
if retention != nil && retUntilDate != nil {
date := *retUntilDate
obj.RetentionMode = string(*retention)
obj.RetentionUntilDate = date.String()
obj.RetentionUntilDate = date.Format(time.RFC3339)
}
}
tags, err := client.getObjectTagging(ctx, bucketName, lsObj.Key, minio.GetObjectTaggingOptions{VersionID: lsObj.VersionID})

View File

@@ -170,23 +170,23 @@ func Test_listObjects(t *testing.T) {
expectedResp: []*models.BucketObject{
&models.BucketObject{
Name: "obj1",
LastModified: t1.String(),
LastModified: t1.Format(time.RFC3339),
Size: int64(1024),
ContentType: "content",
LegalHoldStatus: string(minio.LegalHoldEnabled),
RetentionMode: string(minio.Governance),
RetentionUntilDate: tretention.String(),
RetentionUntilDate: tretention.Format(time.RFC3339),
Tags: map[string]string{
"tag1": "value1",
},
}, &models.BucketObject{
Name: "obj2",
LastModified: t1.String(),
LastModified: t1.Format(time.RFC3339),
Size: int64(512),
ContentType: "content",
LegalHoldStatus: string(minio.LegalHoldEnabled),
RetentionMode: string(minio.Governance),
RetentionUntilDate: tretention.String(),
RetentionUntilDate: tretention.Format(time.RFC3339),
Tags: map[string]string{
"tag1": "value1",
},
@@ -332,18 +332,18 @@ func Test_listObjects(t *testing.T) {
expectedResp: []*models.BucketObject{
&models.BucketObject{
Name: "obj1",
LastModified: t1.String(),
LastModified: t1.Format(time.RFC3339),
Size: int64(1024),
ContentType: "content",
IsDeleteMarker: true,
}, &models.BucketObject{
Name: "obj2",
LastModified: t1.String(),
LastModified: t1.Format(time.RFC3339),
Size: int64(512),
ContentType: "content",
LegalHoldStatus: string(minio.LegalHoldEnabled),
RetentionMode: string(minio.Governance),
RetentionUntilDate: tretention.String(),
RetentionUntilDate: tretention.Format(time.RFC3339),
Tags: map[string]string{
"tag1": "value1",
},
@@ -391,7 +391,7 @@ func Test_listObjects(t *testing.T) {
expectedResp: []*models.BucketObject{
&models.BucketObject{
Name: "obj1",
LastModified: t1.String(),
LastModified: t1.Format(time.RFC3339),
Size: int64(1024),
ContentType: "content",
},
@@ -453,12 +453,12 @@ func Test_listObjects(t *testing.T) {
expectedResp: []*models.BucketObject{
&models.BucketObject{
Name: "obj1",
LastModified: t1.String(),
LastModified: t1.Format(time.RFC3339),
Size: int64(1024),
ContentType: "content",
}, &models.BucketObject{
Name: "obj2",
LastModified: t1.String(),
LastModified: t1.Format(time.RFC3339),
Size: int64(512),
ContentType: "content",
},
@@ -520,12 +520,12 @@ func Test_listObjects(t *testing.T) {
expectedResp: []*models.BucketObject{
&models.BucketObject{
Name: "obj1",
LastModified: t1.String(),
LastModified: t1.Format(time.RFC3339),
Size: int64(1024),
ContentType: "content",
}, &models.BucketObject{
Name: "obj2",
LastModified: t1.String(),
LastModified: t1.Format(time.RFC3339),
Size: int64(512),
ContentType: "content",
},

View File

@@ -1516,6 +1516,17 @@ paths:
get:
summary: Returns information about the deployment
operationId: AdminInfo
parameters:
- name: start
in: query
type: integer
- name: end
in: query
type: integer
- name: step
default: 15
in: query
type: integer
responses:
200:
description: A successful response.
@@ -1656,7 +1667,47 @@ paths:
$ref: "#/definitions/error"
tags:
- UserAPI
/logs/search:
get:
summary: Search the logs
operationId: LogSearch
parameters:
- name: fp
description: Filter Parameters
in: query
collectionFormat: multi
type: array
items:
type: string
- name: pageSize
in: query
type: number
format: int32
default: 10
- name: pageNo
in: query
type: number
format: int32
default: 0
- name: order
in: query
type: string
enum: [ timeDesc, timeAsc ]
default: timeDesc
- name: timeStart
in: query
type: string
responses:
200:
description: A successful response.
schema:
$ref: "#/definitions/logSearchResponse"
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- UserAPI
/tenants:
get:
summary: List Tenant of All Namespaces
@@ -2617,6 +2668,48 @@ definitions:
enum: [ok]
operator:
type: boolean
widgetResult:
type: object
properties:
metric:
type: object
additionalProperties:
type: string
values:
type: array
items: { }
resultTarget:
type: object
properties:
legendFormat:
type: string
resultType:
type: string
result:
type: array
items:
$ref: "#/definitions/widgetResult"
widget:
type: object
properties:
title:
type: string
type:
type: string
options:
type: object
properties:
reduceOptions:
type: object
properties:
calcs:
type: array
items:
type: string
targets:
type: array
items:
$ref: "#/definitions/resultTarget"
adminInfoResponse:
type: object
properties:
@@ -2626,6 +2719,10 @@ definitions:
type: integer
usage:
type: integer
widgets:
type: array
items:
$ref: "#/definitions/widget"
arnsResponse:
type: object
properties:
@@ -2845,6 +2942,14 @@ definitions:
type: boolean
subnet_license:
$ref: "#/definitions/license"
endpoints:
type: object
properties:
minio:
type: string
console:
type: string
tenantUsage:
type: object
properties:
@@ -2877,6 +2982,13 @@ definitions:
namespace:
type: string
logSearchResponse:
type: object
properties:
results:
type: object
title: list of log search responses
listTenantsResponse:
type: object
properties:
@@ -2981,6 +3093,10 @@ definitions:
console:
type: object
$ref: "#/definitions/consoleConfiguration"
expose_minio:
type: boolean
expose_console:
type: boolean
metadataFields:
type: object