Compare commits

...

29 Commits

Author SHA1 Message Date
Harshavardhana
48e6b1bb7c stick to go1.13 for now, update credits (#163)
fix release tags for mcs
2020-06-04 13:15:56 -07:00
César Nieto
8949fbe245 Integrate mkube storageclass api with UI (#156) 2020-06-04 11:22:33 -07:00
Daniel Valdivia
d8e6bd7f4a Fix Add Tenant Image and Delete Tenant URL (#155) 2020-06-04 11:00:28 -07:00
Alex
4edfeb22c6 Removed horizontal scrollbar in menu (#159) 2020-06-04 10:05:04 -07:00
Alex
2d5d0d16ca Changed menu design for mcs (#158)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-06-03 18:56:48 -07:00
César Nieto
16f8ee485a add logs to mkube api handler (#154) 2020-06-02 20:07:58 -07:00
Daniel Valdivia
2d28f8bf35 Pass Storage Class when adding a tenant (#153) 2020-06-02 13:24:07 -05:00
Daniel Valdivia
8af3665ae2 Connect List,Add Tenants (#148) 2020-06-02 11:52:37 -05:00
Daniel Valdivia
0fa1d4bf7c Update Menu with Tools section (#147)
Co-authored-by: Alex <33497058+bexsoft@users.noreply.github.com>
2020-05-28 15:03:29 -07:00
Daniel Valdivia
8139416323 Proxy API For Mkube (#145) 2020-05-27 15:46:18 -07:00
Alex
be5cd7f148 Added flag for operator only features (#144)
Added flag to only enable operator endpoints / links in mcs
2020-05-26 19:35:44 -07:00
César Nieto
fa068b6d4a Add admin heal api and ui (#142) 2020-05-26 17:28:14 -07:00
Alex
a805a49662 Added loaders to bucket information block (#141)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-22 22:46:42 -07:00
Daniel Valdivia
296e4ff5ce Set Policy For Groups (#140) 2020-05-22 16:09:24 -07:00
Alex
20749d2eae Implemented calculation for zone size in zone modal (#137) 2020-05-22 14:49:42 -05:00
Alex
ff4e959d11 Fixed styles in users policy modal (#139)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-22 12:36:41 -07:00
Daniel Valdivia
37195fefa8 Set Policy UI (#138) 2020-05-22 08:48:55 -07:00
Alex
13ef83cee4 Added Clusters mockups (#133) 2020-05-21 20:03:36 -05:00
Alex
b89b2d0c6a Changed bucket detail styles & minor fixes (#136)
Changed bucket detail styles & fixed minor issues for edit access policy & usage report not shown in page

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-21 17:19:50 -07:00
Daniel Valdivia
edf687fd8a Loading text on TableWrapper (#135) 2020-05-21 16:49:31 -07:00
Daniel Valdivia
cb60eba373 Landing images (#134) 2020-05-21 12:55:30 -07:00
Daniel Valdivia
c49a7bbe3c Add details on Policy for Service Account (#132) 2020-05-21 12:11:16 -07:00
Alex
3bb317535c Added path to bucket details in acls list (#131) 2020-05-20 21:59:03 -05:00
Alex
989e6f3471 Added Service Accounts page to settings (#128)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-19 13:41:46 -07:00
Lenin Alevski
35d575e7ac Allow sign-in users without policy (#129) 2020-05-19 15:26:53 -05:00
Harshavardhana
92a8aab07d upgrade linter and cleanup makefile (#126) 2020-05-18 21:55:54 -07:00
César Nieto
c5b2419191 Fix websocket apis (#127)
Remove ping check and instead use a context that will be canceled
if it the client sends a close message or an error occurs on reading.
The context will be used to cancel all functions using it.
2020-05-18 21:21:02 -07:00
Lenin Alevski
732e0ef683 ACL for mcs (#123)
This PR sets the initial version of the ACL for mcs, the idea behind
this is to start using the principle of least privileges when assigning
policies to users when creating users through mcs, currently mcsAdmin policy uses admin:*
and s3:* and by default a user with that policy will have access to everything, if want to limit
that we can create a policy with least privileges.

We need to start validating explicitly if users has acccess to an
specific endpoint based on IAM policy actions.

In this first version every endpoint (you can see it as a page to),
defines a set of well defined admin/s3 actions to work properly, ie:

```
// corresponds to /groups endpoint used by the groups page
var groupsActionSet = iampolicy.NewActionSet(
    iampolicy.ListGroupsAdminAction,
    iampolicy.AddUserToGroupAdminAction,
    //iampolicy.GetGroupAdminAction,
    iampolicy.EnableGroupAdminAction,
    iampolicy.DisableGroupAdminAction,
)

// corresponds to /policies endpoint used by the policies page
var iamPoliciesActionSet = iampolicy.NewActionSet(
    iampolicy.GetPolicyAdminAction,
    iampolicy.DeletePolicyAdminAction,
    iampolicy.CreatePolicyAdminAction,
    iampolicy.AttachPolicyAdminAction,
    iampolicy.ListUserPoliciesAdminAction,
)
```
With that said, for this initial version, now the sessions endpoint will
return a list of authorized pages to be render on the UI, on subsequent
prs we will add this verification of authorization via a server
middleware.
2020-05-18 18:03:06 -07:00
César Nieto
e8491d80cb Add size info to bucket list api (#122)
Using madmin.AccountUsageInfo since that api
includes size already.
Also includes integration with UI.
2020-05-18 13:36:18 -07:00
123 changed files with 11442 additions and 5403 deletions

View File

@@ -3,3 +3,4 @@ dist/
target/
mcs
!mcs/
portal-ui/node_modules/

View File

@@ -14,25 +14,23 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: [1.14.x]
go-version: [1.13.x, 1.14.x]
os: [ubuntu-latest]
steps:
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
uses: actions/setup-go@v1
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Build on ${{ matrix.os }}
env:
GO111MODULE: on
GOOS: linux
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0
$(go env GOPATH)/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
go mod vendor
go test -v -race ./...
make verifiers
make test
make mcs

View File

@@ -1,48 +0,0 @@
name: goreleaser
on:
pull_request:
push:
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Unshallow
run: git fetch --prune --unshallow
-
name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@53acad1befee355d46f71cccf6ab4d885eb4f77f
with:
version: latest
args: release --skip-publish --rm-dist --snapshot
-
name: Upload Win64 Binaries
uses: actions/upload-artifact@v1
if: success()
with:
name: MCS-Snapshot-Build-Win64
path: dist/mcs_windows_amd64
-
name: Upload Linux Binaries
uses: actions/upload-artifact@v1
if: success()
with:
name: MCS-Snapshot-Build-Linux-amd64
path: dist/mcs_linux_amd64
-
name: Upload MacOS Binaries
uses: actions/upload-artifact@v1
if: success()
with:
name: MCS-Snapshot-Build-MacOSX-amd64
path: dist/mcs_darwin_amd64

1
.gitignore vendored
View File

@@ -31,3 +31,4 @@ public.crt
# Ignore VsCode files
.vscode/
*.code-workspace
*~

View File

@@ -1,4 +1,7 @@
linters-settings:
golint:
min-confidence: 0
misspell:
locale: US
@@ -14,4 +17,8 @@ linters:
- gosimple
- deadcode
- unparam
- unused
- structcheck
service:
golangci-lint-version: 1.21.0 # use the fixed version to not introduce new linters unexpectedly

View File

@@ -23,7 +23,7 @@ builds:
- -trimpath
- --tags=kqueue
ldflags:
- -s -w -X github.com/minio/mcs/pkg.ReleaseTag={{.Tag}} -X github.com/minio/warp/pkg.CommitID={{.FullCommit}} -X github.com/minio/warp/pkg.Version={{.Version}} -X github.com/minio/warp/pkg.ShortCommitID={{.ShortCommit}} -X github.com/minio/warp/pkg.ReleaseTime={{.Date}}
- -s -w -X github.com/minio/mcs/pkg.ReleaseTag={{.Tag}} -X github.com/minio/mcs/pkg.CommitID={{.FullCommit}} -X github.com/minio/mcs/pkg.Version={{.Version}} -X github.com/minio/mcs/pkg.ShortCommitID={{.ShortCommit}} -X github.com/minio/mcs/pkg.ReleaseTime={{.Date}}
archives:
-
replacements:

885
CREDITS
View File

@@ -940,6 +940,33 @@ https://github.com/Azure/go-autorest
================================================================
github.com/BurntSushi/toml
https://github.com/BurntSushi/toml
----------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2013 TOML authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================
github.com/PuerkitoBio/purell
https://github.com/PuerkitoBio/purell
----------------------------------------------------------------
@@ -1148,33 +1175,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================
github.com/baiyubin/aliyun-sts-go-sdk
https://github.com/baiyubin/aliyun-sts-go-sdk
----------------------------------------------------------------
The MIT License (MIT)
Copyright (c) Aliyun.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================
github.com/bcicen/jstream
https://github.com/bcicen/jstream
----------------------------------------------------------------
@@ -7113,34 +7113,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
github.com/gorilla/websocket
https://github.com/gorilla/websocket
----------------------------------------------------------------
Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
github.com/grpc-ecosystem/go-grpc-middleware
https://github.com/grpc-ecosystem/go-grpc-middleware
----------------------------------------------------------------
@@ -13042,33 +13014,6 @@ SOFTWARE.
================================================================
github.com/mattn/go-colorable
https://github.com/mattn/go-colorable
----------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2016 Yasuhiro Matsumoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================
github.com/mattn/go-ieproxy
https://github.com/mattn/go-ieproxy
----------------------------------------------------------------
@@ -13113,21 +13058,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
================================================================
github.com/mattn/go-isatty
https://github.com/mattn/go-isatty
----------------------------------------------------------------
Copyright (c) Yasuhiro MATSUMOTO <mattn.jp@gmail.com>
MIT License (Expat)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================================
github.com/mattn/go-runewidth
https://github.com/mattn/go-runewidth
----------------------------------------------------------------
@@ -14107,422 +14037,6 @@ https://github.com/minio/minio
================================================================
github.com/minio/minio
https://github.com/minio/minio
----------------------------------------------------------------
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================================
github.com/minio/minio-go/v6
https://github.com/minio/minio-go/v6
----------------------------------------------------------------
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================================
github.com/minio/minio-go/v6
https://github.com/minio/minio-go/v6
----------------------------------------------------------------
@@ -19918,33 +19432,6 @@ THE SOFTWARE.
================================================================
github.com/sirupsen/logrus
https://github.com/sirupsen/logrus
----------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2014 Simon Eskildsen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================
github.com/smartystreets/assertions
https://github.com/smartystreets/assertions
----------------------------------------------------------------
@@ -21480,6 +20967,31 @@ THE SOFTWARE.
================================================================
go.uber.org/tools
https://go.uber.org/tools
----------------------------------------------------------------
Copyright (c) 2017 Uber Technologies, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================
go.uber.org/zap
https://go.uber.org/zap
----------------------------------------------------------------
@@ -21538,8 +21050,41 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
golang.org/x/crypto
https://golang.org/x/crypto
golang.org/x/lint
https://golang.org/x/lint
----------------------------------------------------------------
Copyright (c) 2013 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
golang.org/x/mod
https://golang.org/x/mod
----------------------------------------------------------------
Copyright (c) 2009 The Go Authors. All rights reserved.
@@ -21802,6 +21347,39 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
golang.org/x/xerrors
https://golang.org/x/xerrors
----------------------------------------------------------------
Copyright (c) 2019 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
google.golang.org/api
https://google.golang.org/api
----------------------------------------------------------------
@@ -22762,203 +22340,6 @@ third-party archives.
================================================================
gopkg.in/ini.v1
https://gopkg.in/ini.v1
----------------------------------------------------------------
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright 2014 Unknwon
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================================
gopkg.in/jcmturner/aescts.v1
https://gopkg.in/jcmturner/aescts.v1
----------------------------------------------------------------
@@ -24463,3 +23844,29 @@ https://gopkg.in/yaml.v2
================================================================
honnef.co/go/tools
https://honnef.co/go/tools
----------------------------------------------------------------
Copyright (c) 2016 Dominik Honnef
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================================

View File

@@ -1,8 +1,9 @@
FROM golang:1.14.1
FROM golang:1.13
ADD go.mod /go/src/github.com/minio/mcs/go.mod
ADD go.sum /go/src/github.com/minio/mcs/go.sum
WORKDIR /go/src/github.com/minio/mcs/
# Get dependencies - will also be cached if we won't change mod/sum
RUN go mod download

View File

@@ -6,7 +6,23 @@ default: mcs
.PHONY: mcs
mcs:
@echo "Building mcs binary to './mcs'"
@(CGO_ENABLED=0 go build -trimpath --tags=kqueue --ldflags "-s -w" -o mcs ./cmd/mcs)
@(GO111MODULE=on CGO_ENABLED=0 go build -trimpath --tags=kqueue --ldflags "-s -w" -o mcs ./cmd/mcs)
getdeps:
@mkdir -p ${GOPATH}/bin
@which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.27.0)
verifiers: getdeps fmt lint
fmt:
@echo "Running $@ check"
@GO111MODULE=on gofmt -d cmd/
@GO111MODULE=on gofmt -d pkg/
lint:
@echo "Running $@ check"
@GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
install: mcs
@echo "Installing mcs binary to '$(GOPATH)/bin/mcs'"
@@ -21,11 +37,11 @@ assets:
@(cd portal-ui; yarn install; make build-static; cd ..)
test:
@(go test -race -v github.com/minio/mcs/restapi/...)
@(go test -race -v github.com/minio/mcs/pkg/auth/...)
@(GO111MODULE=on go test -race -v github.com/minio/mcs/restapi/...)
@(GO111MODULE=on go test -race -v github.com/minio/mcs/pkg/...)
coverage:
@(go test -v -coverprofile=coverage.out github.com/minio/mcs/restapi/... && go tool cover -html=coverage.out && open coverage.html)
@(GO111MODULE=on go test -v -coverprofile=coverage.out github.com/minio/mcs/restapi/... && go tool cover -html=coverage.out && open coverage.html)
clean:
@echo "Cleaning up all the generated files"

View File

@@ -2,6 +2,11 @@
A graphical user interface for [MinIO](https://github.com/minio/minio)
| Dashboard | Adding A User |
| ------------- | ------------- |
| ![Dashboard](images/pic1.png) | ![Dashboard](images/pic2.png) |
## Setup
All `mcs` needs is a MinIO user with admin privileges and URL pointing to your MinIO deployment.
@@ -14,31 +19,30 @@ $ mc admin user add myminio mcs YOURMCSSECRET
$ set -o history
```
2. Create a policy for `mcs`
2. Create a policy for `mcs` with access to everything (for testing and debugging)
```
$ cat > mcsAdmin.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"admin:*"
],
"Effect": "Allow",
"Sid": ""
},
{
"Action": [
"s3:*"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*"
],
"Sid": ""
}
]
"Version": "2012-10-17",
"Statement": [{
"Action": [
"admin:*"
],
"Effect": "Allow",
"Sid": ""
},
{
"Action": [
"s3:*"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*"
],
"Sid": ""
}
]
}
EOF
$ mc admin policy add myminio mcsAdmin mcsAdmin.json
@@ -50,6 +54,49 @@ $ mc admin policy add myminio mcsAdmin mcsAdmin.json
$ mc admin policy set myminio mcsAdmin user=mcs
```
### Note
Additionally, you can create policies to limit the privileges for `mcs` users, for example, if you want the user to only have access to dashboard, buckets, notifications and watch page, the policy should look like this:
```
{
"Version": "2012-10-17",
"Statement": [{
"Action": [
"admin:ServerInfo",
],
"Effect": "Allow",
"Sid": ""
},
{
"Action": [
"s3:ListenBucketNotification",
"s3:PutBucketNotification",
"s3:GetBucketNotification",
"s3:ListMultipartUploadParts",
"s3:ListBucketMultipartUploads",
"s3:ListBucket",
"s3:HeadBucket",
"s3:GetObject",
"s3:GetBucketLocation",
"s3:AbortMultipartUpload",
"s3:CreateBucket",
"s3:PutObject",
"s3:DeleteObject",
"s3:DeleteBucket",
"s3:PutBucketPolicy",
"s3:DeleteBucketPolicy",
"s3:GetBucketPolicy"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*"
],
"Sid": ""
}
]
}
```
## Run MCS server
To run the server:

View File

@@ -21,6 +21,7 @@ import (
"os"
"path/filepath"
"sort"
"time"
"github.com/minio/mcs/pkg"
@@ -103,10 +104,12 @@ func newApp(name string) *cli.App {
app := cli.NewApp()
app.Name = name
app.Version = pkg.Version
app.Version = pkg.Version + " - " + pkg.ShortCommitID
app.Author = "MinIO, Inc."
app.Usage = "mcs"
app.Usage = "MinIO Console Server"
app.Description = `MinIO Console Server`
app.Copyright = "(c) 2020 MinIO, Inc."
app.Compiled, _ = time.Parse(time.RFC3339, pkg.ReleaseTime)
app.Commands = commands
app.HideHelpCommand = true // Hide `help, h` command, we already have `minio --help`.
app.CustomAppHelpTemplate = mcsHelpTemplate

6
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/minio/mcs
go 1.14
go 1.13
require (
github.com/coreos/go-oidc v2.2.1+incompatible
@@ -17,8 +17,8 @@ require (
github.com/jessevdk/go-flags v1.4.0
github.com/json-iterator/go v1.1.9
github.com/minio/cli v1.22.0
github.com/minio/mc v0.0.0-20200515191050-09c31c4ab28c
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0
github.com/minio/mc v0.0.0-20200515235434-3b479cf92ed6
github.com/minio/minio v0.0.0-20200516011754-9cac385aecdb
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/satori/go.uuid v1.2.0

33
go.sum
View File

@@ -239,6 +239,7 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -338,12 +339,16 @@ github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eT
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/cpuid v1.2.2 h1:1xAgYebNnsb9LKCdLOvFWtAxGU/33mjJtyOVbmUa0Us=
github.com/klauspost/cpuid v1.2.2/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.4 h1:EBfaK0SWSwk+fgk6efYFWdzl8MwRWoOO1gkmiaTXPW4=
github.com/klauspost/cpuid v1.2.4/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/readahead v1.3.1 h1:QqXNYvm+VvqYcbrRT4LojUciM0XrznFRIDrbHiJtu/0=
github.com/klauspost/readahead v1.3.1/go.mod h1:AH9juHzNH7xqdqFHrMRSHeH2Ps+vFf+kblDqzPFiLJg=
github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY=
github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
github.com/klauspost/reedsolomon v1.9.7 h1:+azeqnT4iNG9UEcWC+7utJ4xXQ9S8pSlkZor0DOArEQ=
github.com/klauspost/reedsolomon v1.9.7/go.mod h1:+8WD025Xpby8/kG5h/HDPIFhiiuGEtZOKw+5Y4drAD8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -399,15 +404,14 @@ github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2
github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc=
github.com/minio/lsync v1.0.1 h1:AVvILxA976xc27hstd1oR+X9PQG0sPSom1MNb1ImfUs=
github.com/minio/lsync v1.0.1/go.mod h1:tCFzfo0dlvdGl70IT4IAK/5Wtgb0/BrTmo/jE8pArKA=
github.com/minio/mc v0.0.0-20200515191050-09c31c4ab28c h1:G4ZTNwiiJ73folxqNXZpWQofxus2fGJG7hKxYNrtvRM=
github.com/minio/mc v0.0.0-20200515191050-09c31c4ab28c/go.mod h1:U3Jgk0bcSjn+QPUMisrS6nxCWOoQ6rYWSvLCB30apuU=
github.com/minio/mc v0.0.0-20200515235434-3b479cf92ed6 h1:2SrKe2vLDLwvnYkYrJelrzyGW8t/8HCbr9yDsw+8XSI=
github.com/minio/mc v0.0.0-20200515235434-3b479cf92ed6/go.mod h1:U3Jgk0bcSjn+QPUMisrS6nxCWOoQ6rYWSvLCB30apuU=
github.com/minio/minio v0.0.0-20200421050159-282c9f790a03/go.mod h1:zBua5AiljGs1Irdl2XEyiJjvZVCVDIG8gjozzRBcVlw=
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0 h1:QxIz36O01LbKqJiz6HKeKCOC2afgydspkpahQ807msY=
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0/go.mod h1:Vhlqz7Se0EgpgFiVxpvzF4Zz/h2LMx+EPKH96Aera5U=
github.com/minio/minio v0.0.0-20200516011754-9cac385aecdb h1:CQC7D3UDnUycuxhwImcVhMSLet/RbShosAnYcvMtEB8=
github.com/minio/minio v0.0.0-20200516011754-9cac385aecdb/go.mod h1:wymaytM/HELuwdz7BGZHmQ3XKq2SxPsLeGxyOCaCLiA=
github.com/minio/minio-go/v6 v6.0.53 h1:8jzpwiOzZ5Iz7/goFWqNZRICbyWYShbb5rARjrnSCNI=
github.com/minio/minio-go/v6 v6.0.53/go.mod h1:DIvC/IApeHX8q1BAMVCXSXwpmrmM+I+iBvhvztQorfI=
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22 h1:nZEve4vdUhwHBoV18zRvPDgjL6NYyDJE5QJvz3l9bRs=
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
github.com/minio/minio-go/v6 v6.0.55-0.20200425081427-89eebdef2af0/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f h1:ifHrI8+exqLi5RztIWWKS5k+Wu+W7DJisVXwNaCH2zs=
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61 h1:pUSI/WKPdd77gcuoJkSzhJ4wdS8OMDOsOu99MtpXEQA=
@@ -609,11 +613,19 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.11.0 h1:gSmpCfs+R47a4yQPAI4xJ0IPDLTRGXskm6UelqNXpqE=
go.uber.org/zap v1.11.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
golang.org/x/arch v0.0.0-20190909030613-46d78d1859ac/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -622,6 +634,7 @@ golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -637,6 +650,9 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -717,8 +733,11 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190914235951-31e00f45c22e h1:nOOVVcLC+/3MeovP40q5lCiWmP1Z1DaN8yn8ngU63hw=
golang.org/x/tools v0.0.0-20190914235951-31e00f45c22e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32 h1:Xvf3ZQTm5bjXPxhI7g+dwqsCqadK1rcNtwtszuatetk=
golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -788,4 +807,6 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

BIN
images/pic1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

BIN
images/pic2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

View File

@@ -36,6 +36,9 @@ import (
// swagger:model sessionResponse
type SessionResponse struct {
// pages
Pages []string `json:"pages"`
// status
// Enum: [ok]
Status string `json:"status,omitempty"`

29
pkg/acl/config.go Normal file
View File

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

21
pkg/acl/const.go Normal file
View File

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

315
pkg/acl/endpoints.go Normal file
View File

@@ -0,0 +1,315 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package acl
import (
iampolicy "github.com/minio/minio/pkg/iam/policy"
)
// endpoints definition
var (
configuration = "/configurations-list"
users = "/users"
groups = "/groups"
iamPolicies = "/policies"
dashboard = "/dashboard"
profiling = "/profiling"
trace = "/trace"
logs = "/logs"
watch = "/watch"
notifications = "/notification-endpoints"
buckets = "/buckets"
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/service-accounts"
tenants = "/tenants"
tenantsDetail = "/tenants/:tenantName"
heal = "/heal"
)
type ConfigurationActionSet struct {
actionTypes iampolicy.ActionSet
actions iampolicy.ActionSet
}
// configurationActionSet contains the list of admin actions required for this endpoint to work
var configurationActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ConfigUpdateAdminAction,
),
}
// logsActionSet contains the list of admin actions required for this endpoint to work
var logsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ConsoleLogAdminAction,
),
}
// dashboardActionSet contains the list of admin actions required for this endpoint to work
var dashboardActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ServerInfoAdminAction,
),
}
// groupsActionSet contains the list of admin actions required for this endpoint to work
var groupsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListGroupsAdminAction,
iampolicy.AddUserToGroupAdminAction,
//iampolicy.GetGroupAdminAction,
iampolicy.EnableGroupAdminAction,
iampolicy.DisableGroupAdminAction,
),
}
// iamPoliciesActionSet contains the list of admin actions required for this endpoint to work
var iamPoliciesActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.GetPolicyAdminAction,
iampolicy.DeletePolicyAdminAction,
iampolicy.CreatePolicyAdminAction,
iampolicy.AttachPolicyAdminAction,
iampolicy.ListUserPoliciesAdminAction,
),
}
// profilingActionSet contains the list of admin actions required for this endpoint to work
var profilingActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ProfilingAdminAction,
),
}
// traceActionSet contains the list of admin actions required for this endpoint to work
var traceActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.TraceAdminAction,
),
}
// usersActionSet contains the list of admin actions required for this endpoint to work
var usersActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListUsersAdminAction,
iampolicy.CreateUserAdminAction,
iampolicy.DeleteUserAdminAction,
iampolicy.GetUserAdminAction,
iampolicy.EnableUserAdminAction,
iampolicy.DisableUserAdminAction,
),
}
// watchActionSet contains the list of admin actions required for this endpoint to work
var watchActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListenBucketNotificationAction,
),
}
// notificationsActionSet contains the list of admin actions required for this endpoint to work
var notificationsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListenBucketNotificationAction,
iampolicy.PutBucketNotificationAction,
iampolicy.GetBucketNotificationAction,
),
}
// bucketsActionSet contains the list of admin actions required for this endpoint to work
var bucketsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllActions,
),
actions: iampolicy.NewActionSet(
// Read access to buckets
iampolicy.ListMultipartUploadPartsAction,
iampolicy.ListBucketMultipartUploadsAction,
iampolicy.ListBucketAction,
iampolicy.HeadBucketAction,
iampolicy.GetObjectAction,
iampolicy.GetBucketLocationAction,
// Write access to buckets
iampolicy.AbortMultipartUploadAction,
iampolicy.CreateBucketAction,
iampolicy.PutObjectAction,
iampolicy.DeleteObjectAction,
iampolicy.DeleteBucketAction,
// Assign bucket policies
iampolicy.PutBucketPolicyAction,
iampolicy.DeleteBucketPolicyAction,
iampolicy.GetBucketPolicyAction,
),
}
// serviceAccountsActionSet no actions needed for this module to work
var serviceAccountsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
// tenantsActionSet temporally no actions needed for tenants sections to work
var tenantsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
// healActionSet contains the list of admin actions required for this endpoint to work
var healActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.HealAdminAction,
),
}
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet,
users: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
dashboard: dashboardActionSet,
profiling: profilingActionSet,
trace: traceActionSet,
logs: logsActionSet,
watch: watchActionSet,
notifications: notificationsActionSet,
buckets: bucketsActionSet,
bucketsDetail: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
heal: healActionSet,
}
// operatorRules contains the mapping between endpoints and ActionSets for operator only mode
var operatorRules = map[string]ConfigurationActionSet{
tenants: tenantsActionSet,
tenantsDetail: tenantsActionSet,
}
// operatorOnly ENV variable
var operatorOnly = GetOperatorOnly()
// GetActionsStringFromPolicy extract the admin/s3 actions from a given policy and return them in []string format
//
// ie:
// {
// "Version": "2012-10-17",
// "Statement": [{
// "Action": [
// "admin:ServerInfo",
// "admin:CreatePolicy",
// "admin:GetUser"
// ],
// ...
// },
// {
// "Action": [
// "s3:ListenBucketNotification",
// "s3:PutBucketNotification"
// ],
// ...
// }
// ]
// }
// Will produce an array like: ["admin:ServerInfo", "admin:CreatePolicy", "admin:GetUser", "s3:ListenBucketNotification", "s3:PutBucketNotification"]\
func GetActionsStringFromPolicy(policy *iampolicy.Policy) []string {
var actions []string
for _, statement := range policy.Statements {
// We only care about allowed actions
if statement.Effect.IsAllowed(true) {
for _, action := range statement.Actions.ToSlice() {
actions = append(actions, string(action))
}
}
}
return actions
}
// actionsStringToActionSet convert a given string array to iampolicy.ActionSet structure
// this avoids ending with duplicate actions
func actionsStringToActionSet(actions []string) iampolicy.ActionSet {
actionsSet := iampolicy.ActionSet{}
for _, action := range actions {
actionsSet.Add(iampolicy.Action(action))
}
return actionsSet
}
// GetAuthorizedEndpoints return a list of allowed endpoint based on a provided *iampolicy.Policy
// ie: pages the user should have access based on his current privileges
func GetAuthorizedEndpoints(actions []string) []string {
rangeTake := endpointRules
if operatorOnly == "on" {
rangeTake = operatorRules
}
if len(actions) == 0 {
return []string{}
}
// Prepare new ActionSet structure that will hold all the user actions
userAllowedAction := actionsStringToActionSet(actions)
allowedEndpoints := []string{}
for endpoint, rules := range rangeTake {
// check if user policy matches s3:* or admin:* typesIntersection
endpointActionTypes := rules.actionTypes
typesIntersection := endpointActionTypes.Intersection(userAllowedAction)
if len(typesIntersection) == len(endpointActionTypes.ToSlice()) {
allowedEndpoints = append(allowedEndpoints, endpoint)
continue
}
// check if user policy matches explicitly defined endpoint required actions
endpointRequiredActions := rules.actions
actionsIntersection := endpointRequiredActions.Intersection(userAllowedAction)
if len(actionsIntersection) == len(endpointRequiredActions.ToSlice()) {
allowedEndpoints = append(allowedEndpoints, endpoint)
}
}
return allowedEndpoints
}

190
pkg/acl/endpoints_test.go Normal file
View File

@@ -0,0 +1,190 @@
// This file is part of MinIO Orchestrator
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package acl
import (
"reflect"
"testing"
iampolicy "github.com/minio/minio/pkg/iam/policy"
)
type args struct {
actions []string
}
type endpoint struct {
name string
args args
want int
}
func validateEndpoints(t *testing.T, configs []endpoint) {
for _, tt := range configs {
t.Run(tt.name, func(t *testing.T) {
if got := GetAuthorizedEndpoints(tt.args.actions); !reflect.DeepEqual(len(got), tt.want) {
t.Errorf("GetAuthorizedEndpoints() = %v, want %v", len(got), tt.want)
}
})
}
}
func TestGetAuthorizedEndpoints(t *testing.T) {
tests := []endpoint{
{
name: "dashboard endpoint",
args: args{
[]string{"admin:ServerInfo"},
},
want: 2,
},
{
name: "policies endpoint",
args: args{
[]string{
"admin:CreatePolicy",
"admin:DeletePolicy",
"admin:GetPolicy",
"admin:AttachUserOrGroupPolicy",
"admin:ListUserPolicies",
},
},
want: 2,
},
{
name: "all admin endpoints",
args: args{
[]string{
"admin:*",
},
},
want: 11,
},
{
name: "all s3 endpoints",
args: args{
[]string{
"s3:*",
},
},
want: 4,
},
{
name: "all admin and s3 endpoints",
args: args{
[]string{
"admin:*",
"s3:*",
},
},
want: 14,
},
{
name: "no endpoints",
args: args{
[]string{},
},
want: 0,
},
}
validateEndpoints(t, tests)
}
func TestOperatorOnlyEndpoints(t *testing.T) {
operatorOnly = "on"
tests := []endpoint{
{
name: "Operator Only - all admin endpoints",
args: args{
[]string{
"admin:*",
},
},
want: 2,
},
{
name: "Operator Only - all s3 endpoints",
args: args{
[]string{
"s3:*",
},
},
want: 2,
},
{
name: "Operator Only - all admin and s3 endpoints",
args: args{
[]string{
"admin:*",
"s3:*",
},
},
want: 2,
},
{
name: "Operator Only - no endpoints",
args: args{
[]string{},
},
want: 0,
},
}
validateEndpoints(t, tests)
}
func TestGetActionsStringFromPolicy(t *testing.T) {
type args struct {
policy *iampolicy.Policy
}
tests := []struct {
name string
args args
want int
}{
{
name: "parse ReadOnly policy",
args: args{
policy: &iampolicy.ReadOnly,
},
want: 2,
},
{
name: "parse WriteOnly policy",
args: args{
policy: &iampolicy.WriteOnly,
},
want: 1,
},
{
name: "parse AdminDiagnostics policy",
args: args{
policy: &iampolicy.AdminDiagnostics,
},
want: 6,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetActionsStringFromPolicy(tt.args.policy); !reflect.DeepEqual(len(got), tt.want) {
t.Errorf("GetActionsStringFromPolicy() = %v, want %v", len(got), tt.want)
}
})
}
}

View File

@@ -37,7 +37,7 @@ import (
)
var (
errAuthentication = errors.New("Authentication failed, check your access credentials")
errAuthentication = errors.New("authentication failed, check your access credentials")
errNoAuthToken = errors.New("JWT token missing")
errReadingToken = errors.New("JWT internal data is malformed")
errClaimsFormat = errors.New("encrypted jwt claims not in the right format")
@@ -57,6 +57,7 @@ type DecryptedClaims struct {
AccessKeyID string
SecretAccessKey string
SessionToken string
Actions []string
}
// JWTAuthenticate takes a jwt, decode it, extract claims and validate the signature
@@ -93,9 +94,9 @@ func JWTAuthenticate(token string) (*DecryptedClaims, error) {
// NewJWTWithClaimsForClient generates a new jwt with claims based on the provided STS credentials, first
// encrypts the claims and the sign them
func NewJWTWithClaimsForClient(credentials *credentials.Value, audience string) (string, error) {
func NewJWTWithClaimsForClient(credentials *credentials.Value, actions []string, audience string) (string, error) {
if credentials != nil {
encryptedClaims, err := encryptClaims(credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken)
encryptedClaims, err := encryptClaims(credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken, actions)
if err != nil {
return "", err
}
@@ -112,8 +113,8 @@ func NewJWTWithClaimsForClient(credentials *credentials.Value, audience string)
// encryptClaims() receives the 3 STS claims, concatenate them and encrypt them using AES-GCM
// returns a base64 encoded ciphertext
func encryptClaims(accessKeyID, secretAccessKey, sessionToken string) (string, error) {
payload := []byte(fmt.Sprintf("%s:%s:%s", accessKeyID, secretAccessKey, sessionToken))
func encryptClaims(accessKeyID, secretAccessKey, sessionToken string, actions []string) (string, error) {
payload := []byte(fmt.Sprintf("%s#%s#%s#%s", accessKeyID, secretAccessKey, sessionToken, strings.Join(actions, ",")))
ciphertext, err := encrypt(payload)
if err != nil {
return "", err
@@ -133,16 +134,18 @@ func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
log.Println(err)
return nil, errClaimsFormat
}
s := strings.Split(string(plaintext), ":")
s := strings.Split(string(plaintext), "#")
// Validate that the decrypted string has the right format "accessKeyID:secretAccessKey:sessionToken"
if len(s) != 3 {
if len(s) != 4 {
return nil, errClaimsFormat
}
accessKeyID, secretAccessKey, sessionToken := s[0], s[1], s[2]
accessKeyID, secretAccessKey, sessionToken, actions := s[0], s[1], s[2], s[3]
actionsList := strings.Split(actions, ",")
return &DecryptedClaims{
AccessKeyID: accessKeyID,
SecretAccessKey: secretAccessKey,
SessionToken: sessionToken,
Actions: actionsList,
}, nil
}

View File

@@ -37,14 +37,14 @@ func TestNewJWTWithClaimsForClient(t *testing.T) {
funcAssert := assert.New(t)
// Test-1 : NewJWTWithClaimsForClient() is generated correctly without errors
function := "NewJWTWithClaimsForClient()"
jwt, err := NewJWTWithClaimsForClient(creds, audience)
jwt, err := NewJWTWithClaimsForClient(creds, []string{""}, audience)
if err != nil || jwt == "" {
t.Errorf("Failed on %s:, error occurred: %s", function, err)
}
// saving jwt for future tests
goodToken = jwt
// Test-2 : NewJWTWithClaimsForClient() throws error because of empty credentials
if _, err = NewJWTWithClaimsForClient(nil, audience); err != nil {
if _, err = NewJWTWithClaimsForClient(nil, []string{""}, audience); err != nil {
funcAssert.Equal("provided credentials are empty", err.Error())
}
}
@@ -63,7 +63,7 @@ func TestJWTAuthenticate(t *testing.T) {
}
// Test-2 : JWTAuthenticate() return an error because of a tampered jwt
if _, err := JWTAuthenticate(badToken); err != nil {
funcAssert.Equal("Authentication failed, check your access credentials", err.Error())
funcAssert.Equal("authentication failed, check your access credentials", err.Error())
}
// Test-3 : JWTAuthenticate() return an error because of an empty jwt
if _, err := JWTAuthenticate(""); err != nil {

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"@types/webpack-env": "^1.14.1",
"@types/websocket": "^1.0.0",
"ansi-to-react": "^6.0.5",
"chart.js": "^2.9.3",
"codemirror": "^5.52.2",
"history": "^4.10.1",
"local-storage-fallback": "^4.1.1",
@@ -28,6 +29,7 @@
"moment": "^2.24.0",
"npm": "^6.14.4",
"react": "^16.13.1",
"react-chartjs-2": "^2.9.0",
"react-codemirror2": "^7.1.0",
"react-dom": "^16.12.0",
"react-moment": "^0.9.7",

View File

@@ -35,7 +35,7 @@ const isLoggedIn = () => {
};
const mapState = (state: AppState) => ({
loggedIn: state.system.loggedIn
loggedIn: state.system.loggedIn,
});
const connector = connect(mapState, { userLoggedIn });

View File

@@ -24,8 +24,8 @@ export class API {
return request(method, url)
.set("Authorization", `Bearer ${token}`)
.send(data)
.then(res => res.body)
.catch(err => {
.then((res) => res.body)
.catch((err) => {
// if we get unauthorized, kick out the user
if (err.status === 401) {
storage.removeItem("token");

View File

@@ -14,7 +14,18 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
export const units = [
"B",
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
];
export const k8sUnits = ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"];
export const niceBytes = (x: string) => {
let l = 0,
n = parseInt(x, 10) || 0;
@@ -47,3 +58,37 @@ export const timeFromDate = (d: Date) => {
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
};
// units to be used in a dropdown
export const factorForDropdown = () => {
return units.map((unit) => {
return { label: unit, value: unit };
});
};
// units to be used in a dropdown
export const k8sfactorForDropdown = () => {
return k8sUnits.map((unit) => {
return { label: unit, value: unit };
});
};
//getBytes, converts from a value and a unit from units array to bytes
export const getBytes = (value: string, unit: string) => {
const vl: number = parseFloat(value);
const powFactor = units.findIndex((element) => element === unit);
if (powFactor == -1) {
return 0;
}
const factor = Math.pow(1024, powFactor);
const total = vl * factor;
return total.toString(10);
};
//getTotalSize gets the total size of a value & unit
export const getTotalSize = (value: string, unit: string) => {
const bytes = getBytes(value, unit).toString(10);
return niceBytes(bytes);
};

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@@ -14,22 +14,18 @@
// 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 {SvgIcon} from "@material-ui/core";
import { SvgIcon } from "@material-ui/core";
class BucketsIcon extends React.Component {
render() {
return (<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<title>ic_h_buckets</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<polygon className="cls-1" points="13.428 16 2.572 16 0 0 16 0 13.428 16"/>
</g>
</g>
</svg>
</SvgIcon>)
}
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<path d="M8.392,10H1.608L0,0H10Z" />
</svg>
</SvgIcon>
);
}
}
export default BucketsIcon;

View File

@@ -0,0 +1,123 @@
// 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 { SvgIcon } from "@material-ui/core";
class ClustersIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 9">
<g transform="translate(79 438.479)">
<g>
<g>
<rect x="-77.9" y="-434.5" width="7.8" height="1" />
</g>
</g>
<g>
<g>
<rect
x="-77.9"
y="-434.5"
transform="matrix(0.4999 -0.8661 0.8661 0.4999 338.8698 -281.1237)"
width="7.8"
height="1"
/>
</g>
</g>
<g>
<g>
<rect
x="-74.5"
y="-437.9"
transform="matrix(0.866 -0.5001 0.5001 0.866 207.1129 -95.1668)"
width="1"
height="7.8"
/>
</g>
</g>
<g>
<g>
<path
d="M-71.8-430.1h-4.5l-2.2-3.9l2.2-3.9h4.5l2.2,3.9L-71.8-430.1z M-75.7-431.1h3.3l1.7-2.9l-1.7-2.9h-3.3
l-1.7,2.9L-75.7-431.1z"
/>
</g>
</g>
<g>
<g>
<path
d="M-72.3-434c0,0.9-0.7,1.7-1.7,1.7c-0.9,0-1.7-0.7-1.7-1.7c0-0.9,0.7-1.7,1.7-1.7
C-73.1-435.7-72.3-434.9-72.3-434z"
/>
</g>
</g>
<g>
<g>
<path
d="M-76.8-434c0,0.6-0.5,1.1-1.1,1.1c0,0,0,0,0,0c-0.6,0-1.1-0.5-1.1-1.1c0,0,0,0,0,0c0-0.6,0.5-1.1,1.1-1.1
c0,0,0,0,0,0C-77.3-435.1-76.8-434.6-76.8-434C-76.8-434-76.8-434-76.8-434z"
/>
</g>
</g>
<g>
<g>
<path
d="M-69-434c0,0.6-0.5,1.1-1.1,1.1c0,0,0,0,0,0c-0.6,0-1.1-0.5-1.1-1.1c0,0,0,0,0,0c0-0.6,0.5-1.1,1.1-1.1
c0,0,0,0,0,0C-69.5-435.1-69-434.6-69-434C-69-434-69-434-69-434z"
/>
</g>
</g>
<g>
<g>
<path
d="M-75.4-431.6c0.5,0.3,0.7,1,0.4,1.5c-0.3,0.5-1,0.7-1.5,0.4c0,0,0,0,0,0c-0.5-0.3-0.7-1-0.4-1.5
C-76.6-431.7-75.9-431.9-75.4-431.6C-75.4-431.6-75.4-431.6-75.4-431.6z"
/>
</g>
</g>
<g>
<g>
<path
d="M-71.5-438.3c0.5,0.3,0.7,1,0.4,1.5c-0.3,0.5-1,0.7-1.5,0.4c0,0,0,0,0,0c-0.5-0.3-0.7-1-0.4-1.5
C-72.7-438.5-72-438.6-71.5-438.3C-71.5-438.3-71.5-438.3-71.5-438.3z"
/>
</g>
</g>
<g>
<g>
<path
d="M-72.6-431.6c0.5-0.3,1.2-0.1,1.5,0.4c0,0,0,0,0,0c0.3,0.5,0.1,1.2-0.4,1.5c-0.5,0.3-1.2,0.1-1.5-0.4
c0,0,0,0,0,0C-73.3-430.6-73.1-431.3-72.6-431.6z"
/>
</g>
</g>
<g>
<g>
<path
d="M-76.5-438.3c0.5-0.3,1.2-0.1,1.5,0.4c0,0,0,0,0,0c0.3,0.5,0.1,1.2-0.4,1.5c-0.5,0.3-1.2,0.1-1.5-0.4
c0,0,0,0,0,0C-77.2-437.3-77-438-76.5-438.3z"
/>
</g>
</g>
</g>
</svg>
</SvgIcon>
);
}
}
export default ClustersIcon;

View File

@@ -0,0 +1,42 @@
// 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 { SvgIcon } from "@material-ui/core";
class ConfigurationsListIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<rect width="1.433" height="1" />
<rect width="7.828" height="1" transform="translate(2.172)" />
<rect width="1.433" height="1" transform="translate(0 6)" />
<rect width="1.433" height="1" transform="translate(0 3)" />
<rect width="1.433" height="1" transform="translate(0 9)" />
<rect width="1.368" height="0.569" transform="translate(6.316 9)" />
<path d="M5.566,9.569v-.31l-.238-.138-.269.155-.65.375L4.034,9V9H2.172v1H5.566Z" />
<path d="M9.966,9l-.375.65-.65-.375-.269-.155-.238.138V10H10V9H9.967Z" />
<path d="M3.625,6.793l.269-.155V6.362l-.269-.155L3.266,6H2.172V7H3.266Z" />
<path d="M8.434,3.431v.31l.238.138.269-.155.649-.375L9.966,4V4H10V3H8.434Z" />
<path d="M4.034,4l.375-.65.65.375.269.155.238-.138V3H2.172V4H4.033Z" />
<path d="M9.356,5.929,10,5.558,9.316,4.373l-.644.372-.988-.571V3.431H6.316v.743l-.988.571-.644-.372L4,5.558l.644.371V7.071L4,7.442l.684,1.185.644-.372.988.571v.743H7.684V8.826l.988-.571.644.372L10,7.442l-.644-.371ZM7,7.278A.778.778,0,1,1,7.778,6.5.779.779,0,0,1,7,7.278Z" />
</svg>
</SvgIcon>
);
}
}
export default ConfigurationsListIcon;

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@@ -20,15 +20,24 @@ class DashboardIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<title>ic_h_dashboard</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<rect className="cls-1" x="9" width="7" height="7" />
<rect className="cls-1" width="7" height="7" />
<rect className="cls-1" x="9" y="9" width="7" height="7" />
<rect className="cls-1" y="9" width="7" height="7" />
</g>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<g transform="translate(249 720)">
<rect
width="6"
height="5"
transform="translate(-244 -720) rotate(90)"
/>
<rect width="4" height="4" transform="translate(-243 -720)" />
<rect
width="5"
height="4"
transform="translate(-239 -715) rotate(90)"
/>
<rect
width="5"
height="3"
transform="translate(-244 -710) rotate(180)"
/>
</g>
</svg>
</SvgIcon>

View File

@@ -0,0 +1,41 @@
// 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 { SvgIcon } from "@material-ui/core";
class GroupsIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 9.787">
<g transform="translate(177 719.787)">
<g transform="translate(-105 -720)">
<path d="M-65,5a3,3,0,0,0-1.131.224A3.981,3.981,0,0,1-65,8v2h3V8A3,3,0,0,0-65,5Z" />
<path d="M-72,10h6V8a3,3,0,0,0-3-3,3,3,0,0,0-3,3Z" />
<path
className="a"
d="M-65,.213a1.993,1.993,0,0,0-1.384.561A2.967,2.967,0,0,1-66,2.213a2.964,2.964,0,0,1-.384,1.439A1.989,1.989,0,0,0-65,4.213a2,2,0,0,0,2-2A2,2,0,0,0-65,.213Z"
/>
<circle cx="2" cy="2" r="2" transform="translate(-71 0.213)" />
</g>
</g>
</svg>
</SvgIcon>
);
}
}
export default GroupsIcon;

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/>.
import React from "react";
import { SvgIcon } from "@material-ui/core";
class BucketsIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8.75 10">
<path
d="M-44.625,10l-4.353-2.419L-53.375,10V0h8.75Z"
transform="translate(53.375)"
/>
</svg>
</SvgIcon>
);
}
}
export default BucketsIcon;

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/>.
import React from "react";
import { SvgIcon } from "@material-ui/core";
class LambdaNotificationsIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<path
d="M0,0v10l2.8-2.2H10V0H0z M6.6,6L5.6,6.4l-0.8-2l-1.5,2L2.5,5.9l1.9-2.6L4.1,2.4H3.2v-1h1.5l1.4,3.7l0.9-0.4
l0.4,0.9L6.6,6z"
/>
</svg>
</SvgIcon>
);
}
}
export default LambdaNotificationsIcon;

View File

@@ -0,0 +1,35 @@
// 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 { SvgIcon } from "@material-ui/core";
class MirroringIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<g transform="translate(61 439)">
<rect width="1.5" height="10" transform="translate(-56.75 -439)" />
<path d="M6.5,10V0h.572L10,10Z" transform="translate(-61 -439)" />
<path d="M3.5,10V0H2.928L0,10Z" transform="translate(-61 -439)" />
</g>
</svg>
</SvgIcon>
);
}
}
export default MirroringIcon;

View File

@@ -0,0 +1,41 @@
// 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 { SvgIcon } from "@material-ui/core";
class ServiceAccountsIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 9.5">
<g transform="translate(231 719.516)">
<path
d="M-125.5,7.984a4.5,4.5,0,0,1,4.5-4.5,4.5,4.5,0,0,1,4.5,4.5Z"
transform="translate(-105 -720)"
/>
<rect width="10" height="1" transform="translate(-231 -711.016)" />
<path
d="M-119.5.484h-3v1h1v1h1v-1h1Z"
transform="translate(-105 -720)"
/>
</g>
</svg>
</SvgIcon>
);
}
}
export default ServiceAccountsIcon;

View File

@@ -0,0 +1,62 @@
// 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 { SvgIcon } from "@material-ui/core";
class TraceIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9.998 10">
<g transform="translate(140.999 720)">
<g transform="translate(-105 -720)">
<rect
width="1.114"
height="1.667"
transform="translate(-27.116 8.333)"
/>
<path d="M-28.184,10H-29.3V8.154l2.182-3.037V3.147H-26V5.476l-2.182,3.037Z" />
<rect
width="1.114"
height="2.963"
transform="translate(-31.531)"
/>
<rect
width="1.114"
height="2.132"
transform="translate(-27.115 0)"
/>
<rect
width="1.114"
height="5.389"
transform="translate(-29.298)"
/>
<path d="M-30.417,10h-1.114V5.722l-2.233-3V0h1.114V2.353l2.233,3Z" />
<path d="M-32.65,10h-1.114V6.185l-2.234-3V0h1.114V2.815l2.234,3Z" />
<rect
width="1.114"
height="4.463"
transform="translate(-35.999 5.537)"
/>
</g>
</g>
</svg>
</SvgIcon>
);
}
}
export default TraceIcon;

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@@ -14,27 +14,32 @@
// 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 {SvgIcon} from "@material-ui/core";
import { SvgIcon } from "@material-ui/core";
class UsersIcon extends React.Component {
render() {
return (<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 14.874">
<title>ic_users</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path className="cls-1"
d="M3.5,6.875h0a3.5,3.5,0,0,1,3.5,3.5v4.5a0,0,0,0,1,0,0H0a0,0,0,0,1,0,0v-4.5A3.5,3.5,0,0,1,3.5,6.875Z"/>
<path className="cls-1"
d="M12.5,6.875h0a3.5,3.5,0,0,1,3.5,3.5v4.5a0,0,0,0,1,0,0H9a0,0,0,0,1,0,0v-4.5A3.5,3.5,0,0,1,12.5,6.875Z"/>
<circle className="cls-1" cx="3.498" cy="2.859" r="2.859"/>
<circle className="cls-1" cx="12.502" cy="2.859" r="2.859"/>
</g>
</g>
</svg>
</SvgIcon>)
}
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 6.131 10">
<g transform="translate(193 719.787)">
<g transform="translate(-193 -719.787)">
<path
d="M3,0h.131a3,3,0,0,1,3,3V5a0,0,0,0,1,0,0H0A0,0,0,0,1,0,5V3A3,3,0,0,1,3,0Z"
transform="translate(0 5)"
/>
<ellipse
cx="2.065"
cy="2"
rx="2.065"
ry="2"
transform="translate(1 0)"
/>
</g>
</g>
</svg>
</SvgIcon>
);
}
}
export default UsersIcon;

View File

@@ -0,0 +1,39 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { SvgIcon } from "@material-ui/core";
class WarpIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<g transform="translate(43 439)">
<path d="M27.5,10" transform="translate(-61 -439)" />
<rect width="1.5" height="2" transform="translate(-43 -431)" />
<rect width="1.5" height="6" transform="translate(-38.75 -435)" />
<rect width="1.5" height="8" transform="translate(-36.625 -437)" />
<rect width="1.5" height="4" transform="translate(-40.875 -433)" />
<rect width="1.5" height="10" transform="translate(-34.5 -439)" />
<path d="M18.5,10" transform="translate(-61 -439)" />
</g>
</svg>
</SvgIcon>
);
}
}
export default WarpIcon;

View File

@@ -0,0 +1,59 @@
// 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 { SvgIcon } from "@material-ui/core";
class WatchIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<g transform="translate(213 720)">
<g transform="translate(-105 -720)">
<rect width="1.5" height="4" transform="translate(-108)" />
<rect width="1.5" height="4" transform="translate(-108 6)" />
<rect width="1.5" height="4" transform="translate(-99.5 6)" />
<rect width="1.5" height="4" transform="translate(-99.5)" />
<rect
width="1.5"
height="4"
transform="translate(-98) rotate(90)"
/>
<rect
width="1.5"
height="4"
transform="translate(-104) rotate(90)"
/>
<rect
width="1.5"
height="4"
transform="translate(-104 8.5) rotate(90)"
/>
<rect
width="1.5"
height="4"
transform="translate(-98 8.5) rotate(90)"
/>
<circle cx="2" cy="2" r="2" transform="translate(-105 3)" />
</g>
</g>
</svg>
</SvgIcon>
);
}
}
export default WatchIcon;

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@@ -14,10 +14,20 @@
// 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 { default as PermissionIcon } from './PermissionIcon';
export { default as CreateIcon } from './CreateIcon';
export { default as DeleteIcon } from './DeleteIcon';
export { default as ServiceAccountIcon } from './ServiceAccountIcon';
export { default as DashboardIcon } from './DashboardIcon';
export { default as BucketsIcon } from './BucketsIcon';
export { default as UsersIcon } from './UsersIcon';
export { default as PermissionIcon } from "./PermissionIcon";
export { default as CreateIcon } from "./CreateIcon";
export { default as DeleteIcon } from "./DeleteIcon";
export { default as ServiceAccountIcon } from "./ServiceAccountIcon";
export { default as DashboardIcon } from "./DashboardIcon";
export { default as BucketsIcon } from "./BucketsIcon";
export { default as UsersIcon } from "./UsersIcon";
export { default as ServiceAccountsIcon } from "./ServiceAccountsIcon";
export { default as GroupsIcon } from "./GroupsIcon";
export { default as IAMPoliciesIcon } from "./IAMPoliciesIcon";
export { default as TraceIcon } from "./TraceIcon";
export { default as LambdaNotificationsIcon } from "./LambdaNotificationsIcon";
export { default as ConfigurationsListIcon } from "./ConfigurationsListIcon";
export { default as ClustersIcon } from "./ClustersIcon";
export { default as MirroringIcon } from "./MirroringIcon";
export { default as WarpIcon } from "./WarpIcon";
export { default as WatchIcon } from "./WatchIcon";

View File

@@ -30,49 +30,50 @@ import AddBucket from "./AddBucket";
import DeleteBucket from "./DeleteBucket";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../../../icons";
import { niceBytes } from "../../../../common/utils";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
}
boxShadow: "0px 3px 6px #00000012",
},
});
interface IListBucketsProps {
@@ -108,7 +109,7 @@ class ListBuckets extends React.Component<
rowsPerPage: 10,
deleteOpen: false,
selectedBucket: "",
filterBuckets: ""
filterBuckets: "",
};
fetchRecords() {
@@ -122,7 +123,7 @@ class ListBuckets extends React.Component<
loading: false,
records: res.buckets || [],
totalRecords: !res.buckets ? 0 : res.total,
error: ""
error: "",
});
// if we get 0 results, and page > 0 , go down 1 page
if (
@@ -174,7 +175,7 @@ class ListBuckets extends React.Component<
rowsPerPage,
deleteOpen,
selectedBucket,
filterBuckets
filterBuckets,
} = this.state;
const offset = page * rowsPerPage;
@@ -196,7 +197,7 @@ class ListBuckets extends React.Component<
const tableActions = [
{ type: "view", to: `/buckets`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteBucket, sendOnlyId: true }
{ type: "delete", onClick: confirmDeleteBucket, sendOnlyId: true },
];
const displayParsedDate = (date: string) => {
@@ -249,9 +250,9 @@ class ListBuckets extends React.Component<
className={classes.searchField}
id="search-resource"
label=""
onChange={val => {
onChange={(val) => {
this.setState({
filterBuckets: val.target.value
filterBuckets: val.target.value,
});
}}
InputProps={{
@@ -260,7 +261,7 @@ class ListBuckets extends React.Component<
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
/>
<Button
@@ -269,7 +270,7 @@ class ListBuckets extends React.Component<
startIcon={<CreateIcon />}
onClick={() => {
this.setState({
addScreenOpen: true
addScreenOpen: true,
});
}}
>
@@ -287,8 +288,13 @@ class ListBuckets extends React.Component<
{
label: "Creation Date",
elementKey: "creation_date",
renderFunction: displayParsedDate
}
renderFunction: displayParsedDate,
},
{
label: "Size",
elementKey: "size",
renderFunction: niceBytes,
},
]}
isLoading={loading}
records={filteredRecords}
@@ -302,11 +308,11 @@ class ListBuckets extends React.Component<
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>

View File

@@ -35,6 +35,7 @@ interface ISetAccessPolicyProps {
classes: any;
open: boolean;
bucketName: string;
actualPolicy: string;
closeModalAndRefresh: () => void;
}
@@ -86,8 +87,14 @@ class SetAccessPolicy extends React.Component<
});
}
componentDidMount() {
const { actualPolicy } = this.props;
this.setState({ accessPolicy: actualPolicy });
}
render() {
const { classes, open } = this.props;
const { classes, open, actualPolicy } = this.props;
const { addLoading, addError, accessPolicy } = this.state;
return (
<ModalWrapper

View File

@@ -17,9 +17,13 @@
import React from "react";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import CircularProgress from "@material-ui/core/CircularProgress";
import api from "../../../../common/api";
import { BucketEvent, BucketEventList, BucketInfo } from "../types";
import { BucketEvent, BucketEventList, BucketInfo, BucketList } from "../types";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import SetAccessPolicy from "./SetAccessPolicy";
@@ -28,6 +32,7 @@ import { CreateIcon } from "../../../../icons";
import AddEvent from "./AddEvent";
import DeleteEvent from "./DeleteEvent";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { niceBytes } from "../../../../common/utils";
const styles = (theme: Theme) =>
createStyles({
@@ -76,6 +81,42 @@ const styles = (theme: Theme) =>
textAlign: "center",
padding: "20px",
},
gridContainer: {
display: "grid",
gridTemplateColumns: "auto auto",
gridGap: 8,
justifyContent: "flex-start",
alignItems: "center",
"& div:not(.MuiCircularProgress-root)": {
display: "flex",
alignItems: "center",
},
"& div:nth-child(odd)": {
justifyContent: "flex-end",
fontWeight: 700,
},
"& div:nth-child(2n)": {
minWidth: 150,
},
},
masterActions: {
width: "25%",
minWidth: "120px",
"& div": {
margin: "5px 0px",
},
},
paperContainer: {
padding: 15,
paddingLeft: 23,
},
headerContainer: {
display: "flex",
justifyContent: "space-between",
},
capitalizeFirst: {
textTransform: "capitalize",
},
});
interface IViewBucketProps {
@@ -87,9 +128,12 @@ interface IViewBucketState {
info: BucketInfo | null;
records: BucketEvent[];
totalRecords: number;
loading: boolean;
loadingBucket: boolean;
loadingEvents: boolean;
loadingSize: boolean;
error: string;
deleteError: string;
errBucket: string;
setAccessPolicyScreenOpen: boolean;
page: number;
rowsPerPage: number;
@@ -97,6 +141,8 @@ interface IViewBucketState {
deleteOpen: boolean;
selectedBucket: string;
selectedEvent: BucketEvent | null;
bucketSize: string;
errorSize: string;
}
class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
@@ -104,9 +150,12 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
info: null,
records: [],
totalRecords: 0,
loading: false,
loadingBucket: true,
loadingEvents: true,
loadingSize: true,
error: "",
deleteError: "",
errBucket: "",
setAccessPolicyScreenOpen: false,
page: 0,
rowsPerPage: 10,
@@ -114,10 +163,12 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
deleteOpen: false,
selectedBucket: "",
selectedEvent: null,
bucketSize: "0",
errorSize: "",
};
fetchEvents() {
this.setState({ loading: true }, () => {
this.setState({ loadingBucket: true }, () => {
const { page } = this.state;
const { match } = this.props;
const bucketName = match.params["bucketName"];
@@ -128,7 +179,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
const total = get(res, "total", 0);
this.setState({
loading: false,
loadingEvents: false,
records: events || [],
totalRecords: total,
error: "",
@@ -142,7 +193,50 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
}
})
.catch((err: any) => {
this.setState({ loading: false, error: err });
this.setState({ loadingEvents: false, error: err });
});
});
}
fetchBucketsSize() {
const { match } = this.props;
const bucketName = match.params["bucketName"];
this.setState({ loadingSize: true }, () => {
api
.invoke("GET", `/api/v1/buckets`)
.then((res: BucketList) => {
const resBuckets = get(res, "buckets", []);
const bucketInfo = resBuckets.find(
(bucket) => bucket.name === bucketName
);
const size = get(bucketInfo, "size", "0");
this.setState({
loadingSize: false,
errorSize: "",
bucketSize: size,
});
})
.catch((err: any) => {
this.setState({ loadingSize: false, errorSize: err });
});
});
}
loadInfo() {
const { match } = this.props;
const bucketName = match.params["bucketName"];
this.setState({ loadingBucket: true }, () => {
api
.invoke("GET", `/api/v1/buckets/${bucketName}`)
.then((res: BucketInfo) => {
this.setState({ loadingBucket: false, info: res });
})
.catch((err) => {
this.setState({ loadingBucket: false, errBucket: err });
});
});
}
@@ -161,20 +255,10 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
});
}
loadInfo() {
const { match } = this.props;
const bucketName = match.params["bucketName"];
api
.invoke("GET", `/api/v1/buckets/${bucketName}`)
.then((res: BucketInfo) => {
this.setState({ info: res });
})
.catch((err) => {});
}
componentDidMount(): void {
this.loadInfo();
this.fetchEvents();
this.fetchBucketsSize();
}
bucketFilter(): void {}
@@ -186,12 +270,15 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
records,
totalRecords,
setAccessPolicyScreenOpen,
loading,
loadingEvents,
loadingBucket,
page,
rowsPerPage,
deleteOpen,
addScreenOpen,
selectedEvent,
bucketSize,
loadingSize,
} = this.state;
const offset = page * rowsPerPage;
@@ -242,6 +329,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
<SetAccessPolicy
bucketName={bucketName}
open={setAccessPolicyScreenOpen}
actualPolicy={accessPolicy}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
@@ -258,36 +346,75 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
<br />
</Grid>
<Grid item xs={12}>
Access Policy: {accessPolicy}
{" "}
<Button
variant="contained"
size="small"
color="primary"
onClick={() => {
this.setState({
setAccessPolicyScreenOpen: true,
});
}}
>
Change Access Policy
</Button>
<br />
Reported Usage: 0 bytes
<br />
<div className={classes.headerContainer}>
<div>
<Paper className={classes.paperContainer}>
<div className={classes.gridContainer}>
<div>Access Policy:</div>
<div className={classes.capitalizeFirst}>
{loadingBucket ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
accessPolicy.toLowerCase()
)}
</div>
<div>Reported Usage:</div>
<div>
{loadingSize ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
niceBytes(bucketSize)
)}
</div>
</div>
</Paper>
</div>
<div className={classes.masterActions}>
<div>
<Button
variant="contained"
color="primary"
fullWidth
size="medium"
onClick={() => {
this.setState({
setAccessPolicyScreenOpen: true,
});
}}
>
Change Access Policy
</Button>
</div>
</div>
</div>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={6}>
<Typography variant="h6">Events</Typography>
<Tabs
value={0}
indicatorColor="primary"
textColor="primary"
aria-label="cluster-tabs"
>
<Tab label="Events" />
</Tabs>
</Grid>
<Grid item xs={6} className={classes.actionsTray}>
<Button
variant="contained"
color="primary"
size="small"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
this.setState({
addScreenOpen: true,
@@ -313,7 +440,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
{ label: "Prefix", elementKey: "prefix" },
{ label: "Suffix", elementKey: "suffix" },
]}
isLoading={loading}
isLoading={loadingEvents}
records={filteredRecords}
entityName="Events"
idField="id"

View File

@@ -0,0 +1,107 @@
// 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 {
Checkbox,
Grid,
InputLabel,
TextField,
TextFieldProps,
Tooltip,
} from "@material-ui/core";
import { OutlinedInputProps } from "@material-ui/core/OutlinedInput";
import {
createStyles,
makeStyles,
Theme,
withStyles,
} from "@material-ui/core/styles";
import {
checkboxIcons,
fieldBasic,
tooltipHelper,
} from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface CheckBoxProps {
label: string;
classes: any;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
value: string | boolean;
id: string;
name: string;
disabled?: boolean;
tooltip?: string;
index?: number;
checked: boolean;
}
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
...checkboxIcons,
labelContainer: {
flexGrow: 1,
},
});
const CheckboxWrapper = ({
label,
onChange,
value,
id,
name,
checked = false,
disabled = false,
tooltip = "",
classes,
}: CheckBoxProps) => {
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
)}
<div className={classes.labelContainer}>
<Checkbox
name={name}
id={id}
value={value}
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={checked}
onChange={onChange}
checkedIcon={<span className={classes.checkedIcon} />}
icon={<span className={classes.unCheckedIcon} />}
disabled={disabled}
/>
</div>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(CheckboxWrapper);

View File

@@ -92,16 +92,18 @@ const SelectWrapper = ({
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
)}
<FormControl variant="outlined" fullWidth>
<Select
id={id}

View File

@@ -61,3 +61,18 @@ export const tooltipHelper = {
fontSize: 18,
},
};
const checkBoxBasic = {
width: 16,
height: 16,
borderRadius: 3,
};
export const checkboxIcons = {
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #d0d0d0" },
checkedIcon: {
...checkBoxBasic,
border: "1px solid #201763",
backgroundColor: "#201763",
},
};

View File

@@ -19,6 +19,7 @@ import { IconButton } from "@material-ui/core";
import ViewIcon from "./TableActionIcons/ViewIcon";
import PencilIcon from "./TableActionIcons/PencilIcon";
import DeleteIcon from "./TableActionIcons/DeleteIcon";
import DescriptionIcon from "./TableActionIcons/DescriptionIcon";
import { Link } from "react-router-dom";
interface IActionButton {
@@ -39,6 +40,8 @@ const defineIcon = (type: string, selected: boolean) => {
return <PencilIcon active={selected} />;
case "delete":
return <DeleteIcon active={selected} />;
case "description":
return <DescriptionIcon active={selected} />;
}
return null;
@@ -51,13 +54,14 @@ const TableActionButton = ({
idField,
selected,
to,
sendOnlyId = false
sendOnlyId = false,
}: IActionButton) => {
const valueClick = sendOnlyId ? valueToSend[idField] : valueToSend;
const buttonElement = (
<IconButton
aria-label={type}
size={"small"}
onClick={
onClick
? () => {

View File

@@ -0,0 +1,20 @@
import React from "react";
import { IIcon, selected, unSelected } from "./common";
const PencilIcon = ({ active = false }: IIcon) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill={active ? selected : unSelected}
d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"
></path>
</svg>
);
};
export default PencilIcon;

View File

@@ -26,11 +26,13 @@ import {
TableRow,
Paper,
Grid,
Checkbox
Checkbox,
Typography,
} from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { TablePaginationActionsProps } from "@material-ui/core/TablePagination/TablePaginationActions";
import TableActionButton from "./TableActionButton";
import { checkboxIcons } from "../FormComponents/common/styleLibrary";
//Interfaces for table Items
@@ -84,25 +86,20 @@ const borderColor = "#eaeaea";
const rowText = {
fontWeight: 400,
fontSize: 14,
borderColor: borderColor
};
const checkBoxBasic = {
width: 16,
height: 16,
borderRadius: 3
borderColor: borderColor,
};
const styles = (theme: Theme) =>
createStyles({
dialogContainer: {
padding: "12px 26px 22px"
padding: "12px 26px 22px",
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column",
padding: "19px 38px"
padding: "19px 38px",
minHeight: "200px",
},
minTableHeader: {
color: "#393939",
@@ -111,43 +108,42 @@ const styles = (theme: Theme) =>
fontWeight: 700,
fontSize: 14,
paddingBottom: 15,
borderColor: borderColor
}
}
borderColor: borderColor,
},
},
},
rowUnselected: {
...rowText
...rowText,
},
rowSelected: {
...rowText,
color: "#201763"
color: "#201763",
},
paginatorContainer: {
display: "flex",
justifyContent: "flex-end",
padding: "5px 38px"
padding: "5px 38px",
},
checkBoxHeader: {
"&.MuiTableCell-paddingCheckbox": {
paddingBottom: 9
}
paddingBottom: 9,
},
},
actionsContainer: {
width: 120,
borderColor: borderColor
width: 150,
borderColor: borderColor,
},
paginatorComponent: {
borderBottom: 0
},
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #d0d0d0" },
checkedIcon: {
...checkBoxBasic,
border: "1px solid #201763",
backgroundColor: "#201763"
borderBottom: 0,
},
checkBoxRow: {
borderColor: borderColor
}
borderColor: borderColor,
},
loadingBox: {
paddingTop: "100px",
paddingBottom: "100px",
},
...checkboxIcons,
});
// Function that renders Title Columns
@@ -221,12 +217,21 @@ const TableWrapper = ({
idField,
classes,
stickyHeader = false,
paginatorConfig
paginatorConfig,
}: TableWrapperProps) => {
return (
<Grid item xs={12}>
<Paper className={classes.paper}>
{isLoading && <LinearProgress />}
{isLoading && (
<Grid container className={classes.loadingBox}>
<Grid item xs={12} style={{ textAlign: "center" }}>
<Typography component="h3">Loading...</Typography>
</Grid>
<Grid item xs={12}>
<LinearProgress />
</Grid>
</Grid>
)}
{records && records.length > 0 ? (
<Table size="small" stickyHeader={stickyHeader}>
<TableHead className={classes.minTableHeader}>
@@ -298,7 +303,9 @@ const TableWrapper = ({
</TableBody>
</Table>
) : (
<div>{`There are no ${entityName} yet.`}</div>
<React.Fragment>
{!isLoading && <div>{`There are no ${entityName} yet.`}</div>}
</React.Fragment>
)}
</Paper>
{paginatorConfig && (

View File

@@ -35,36 +35,36 @@ interface IListConfiguration {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
boxShadow: "0px 3px 6px #00000012",
},
iconText: {
lineHeight: "24px"
}
lineHeight: "24px",
},
});
const ConfigurationsList = ({ classes }: IListConfiguration) => {
const [editScreenOpen, setEditScreenOpen] = useState(false);
const [selectedConfiguration, setSelectedConfiguration] = useState({
configuration_id: "",
configuration_label: ""
configuration_label: "",
});
const [error, setError] = useState("");
const [filter, setFilter] = useState("");
@@ -81,12 +81,12 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
setSelectedConfiguration(element);
setEditScreenOpen(true);
}
}
}
},
},
];
const filteredRecords: IConfigurationElement[] = configurationElements.filter(
elementItem =>
(elementItem) =>
elementItem.configuration_id
.toLocaleLowerCase()
.includes(filter.toLocaleLowerCase())
@@ -117,7 +117,7 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
className={classes.searchField}
id="search-resource"
label=""
onChange={event => {
onChange={(event) => {
setFilter(event.target.value);
}}
InputProps={{
@@ -126,7 +126,7 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
/>
</Grid>
@@ -137,7 +137,7 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Configuration", elementKey: "configuration_id" }
{ label: "Configuration", elementKey: "configuration_id" },
]}
isLoading={false}
records={filteredRecords}

View File

@@ -44,29 +44,29 @@ interface IWebhook {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
boxShadow: "0px 3px 6px #00000012",
},
iconText: {
lineHeight: "24px"
}
lineHeight: "24px",
},
});
const panels = {
@@ -77,8 +77,8 @@ const panels = {
apiURL: "",
configuration: {
configuration_id: "logger_webhook",
configuration_label: "Logger Webhook"
}
configuration_label: "Logger Webhook",
},
},
audit: {
main: "audit",
@@ -87,9 +87,9 @@ const panels = {
apiURL: "",
configuration: {
configuration_id: "audit_webhook",
configuration_label: "Audit Webhook"
}
}
configuration_label: "Audit Webhook",
},
},
};
const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
@@ -107,15 +107,15 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
return null;
}
const filteredRecords: IWebhook[] = webhooks.filter(elementItem =>
const filteredRecords: IWebhook[] = webhooks.filter((elementItem) =>
elementItem.name.toLocaleLowerCase().includes(filter.toLocaleLowerCase())
);
const tableActions = [
{
type: "edit",
onClick: () => {}
}
onClick: () => {},
},
];
return (
@@ -144,7 +144,7 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
className={classes.searchField}
id="search-resource"
label=""
onChange={event => {
onChange={(event) => {
setFilter(event.target.value);
}}
InputProps={{
@@ -153,7 +153,7 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
/>
<Button

View File

@@ -31,34 +31,34 @@ export const configurationElements: IConfigurationElement[] = [
{ configuration_id: "cache", configuration_label: "Cache Configuration" },
{
configuration_id: "compression",
configuration_label: "Compression Configuration"
configuration_label: "Compression Configuration",
},
{ configuration_id: "etcd", configuration_label: "Etcd Configuration" },
{
configuration_id: "identity_openid",
configuration_label: "Identity Openid Configuration"
configuration_label: "Identity Openid Configuration",
},
{
configuration_id: "identity_ldap",
configuration_label: "Identity LDAP Configuration"
configuration_label: "Identity LDAP Configuration",
},
{
configuration_id: "policy_opa",
configuration_label: "Policy OPA Configuration"
configuration_label: "Policy OPA Configuration",
},
{
configuration_id: "kms_vault",
configuration_label: "KMS Vault Configuration"
configuration_label: "KMS Vault Configuration",
},
{ configuration_id: "kms_kes", configuration_label: "KMS KES Configuration" },
{
configuration_id: "logger_webhook",
configuration_label: "Logger Webhook Configuration"
configuration_label: "Logger Webhook Configuration",
},
{
configuration_id: "audit_webhook",
configuration_label: "Audit Webhook Configuration"
}
configuration_label: "Audit Webhook Configuration",
},
];
export const fieldsConfigurations: any = {
@@ -68,7 +68,7 @@ export const fieldsConfigurations: any = {
required: true,
label: "name",
tooltip: 'Name of the location of the server e.g. "us-west-rack2"',
type: "string"
type: "string",
},
{
name: "comment",
@@ -76,8 +76,8 @@ export const fieldsConfigurations: any = {
label: "comment",
tooltip: "You can add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
cache: [
{
@@ -86,21 +86,21 @@ export const fieldsConfigurations: any = {
label: "Drives",
tooltip:
'Mountpoints e.g. "/optane1" or "/optane2", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "expiry",
required: false,
label: "Expiry",
tooltip: 'Cache expiry duration in days e.g. "90"',
type: "number"
type: "number",
},
{
name: "quota",
required: false,
label: "Quota",
tooltip: 'Limit cache drive usage in percentage e.g. "90"',
type: "number"
type: "number",
},
{
name: "exclude",
@@ -108,28 +108,28 @@ export const fieldsConfigurations: any = {
label: "Exclude",
tooltip:
'Wildcard exclusion patterns e.g. "bucket/*.tmp" or "*.exe", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "after",
required: false,
label: "After",
tooltip: "Minimum number of access before caching an object",
type: "number"
type: "number",
},
{
name: "watermark_low",
required: false,
label: "Watermark Low",
tooltip: "Watermark Low",
type: "number"
type: "number",
},
{
name: "watermark_high",
required: false,
label: "Watermark High",
tooltip: "Watermark High",
type: "number"
type: "number",
},
{
name: "comment",
@@ -137,8 +137,8 @@ export const fieldsConfigurations: any = {
label: "Comment",
tooltip: "You can add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
compression: [
{
@@ -147,7 +147,7 @@ export const fieldsConfigurations: any = {
label: "Extensions",
tooltip:
'Extensions to compress e.g. ".txt",".log" or ".csv", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "mime_types",
@@ -155,8 +155,8 @@ export const fieldsConfigurations: any = {
label: "Mime Types",
tooltip:
'Mime types e.g. "text/*","application/json" or "application/xml", you can write one per field',
type: "csv"
}
type: "csv",
},
],
etcd: [
{
@@ -165,35 +165,35 @@ export const fieldsConfigurations: any = {
label: "Endpoints",
tooltip:
'List of etcd endpoints e.g. "http://localhost:2379", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "path_prefix",
required: false,
label: "Path Prefix",
tooltip: 'namespace prefix to isolate tenants e.g. "customer1/"',
type: "string"
type: "string",
},
{
name: "coredns_path",
required: false,
label: "Coredns Path",
tooltip: 'Shared bucket DNS records, default is "/skydns"',
type: "string"
type: "string",
},
{
name: "client_cert",
required: false,
label: "Client Cert",
tooltip: "Client cert for mTLS authentication",
type: "string"
type: "string",
},
{
name: "client_cert_key",
required: false,
label: "Client Cert Key",
tooltip: "Client cert key for mTLS authentication",
type: "string"
type: "string",
},
{
name: "comment",
@@ -201,8 +201,8 @@ export const fieldsConfigurations: any = {
label: "Comment",
tooltip: "You can add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
identity_openid: [
{
@@ -210,28 +210,28 @@ export const fieldsConfigurations: any = {
required: false,
label: "Config URL",
tooltip: "Config URL for Client ID configuration",
type: "string"
type: "string",
},
{
name: "client_id",
required: false,
label: "Client ID",
type: "string"
type: "string",
},
{
name: "claim_name",
required: false,
label: "Claim Name",
tooltip: "Claim Name",
type: "string"
type: "string",
},
{
name: "claim_prefix",
required: false,
label: "Claim Prefix",
tooltip: "Claim Prefix",
type: "string"
}
type: "string",
},
],
identity_ldap: [
{
@@ -239,7 +239,7 @@ export const fieldsConfigurations: any = {
required: true,
label: "Server ADDR",
tooltip: 'AD/LDAP server address e.g. "myldapserver.com:636"',
type: "string"
type: "string",
},
{
name: "username_format",
@@ -247,7 +247,7 @@ export const fieldsConfigurations: any = {
label: "Username Format",
tooltip:
'List of username bind DNs e.g. "uid=%s","cn=accounts","dc=myldapserver" or "dc=com", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "username_search_filter",
@@ -255,7 +255,7 @@ export const fieldsConfigurations: any = {
label: "Username Search Filter",
tooltip:
'User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"',
type: "string"
type: "string",
},
{
name: "group_search_filter",
@@ -263,21 +263,21 @@ export const fieldsConfigurations: any = {
label: "Group Search Filter",
tooltip:
'Search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))"',
type: "string"
type: "string",
},
{
name: "username_search_base_dn",
required: false,
label: "Username Search Base DN",
tooltip: "List of username search DNs, you can write one per field",
type: "csv"
type: "csv",
},
{
name: "group_name_attribute",
required: false,
label: "Group Name Attribute",
tooltip: 'Search attribute for group name e.g. "cn"',
type: "string"
type: "string",
},
{
name: "sts_expiry",
@@ -285,7 +285,7 @@ export const fieldsConfigurations: any = {
label: "STS Expiry",
tooltip:
'temporary credentials validity duration in s,m,h,d. Default is "1h"',
type: "string"
type: "string",
},
{
name: "tls_skip_verify",
@@ -293,7 +293,7 @@ export const fieldsConfigurations: any = {
label: "TLS Skip Verify",
tooltip:
'Trust server TLS without verification, defaults to "off" (verify)',
type: "on|off"
type: "on|off",
},
{
name: "server_insecure",
@@ -301,7 +301,7 @@ export const fieldsConfigurations: any = {
label: "Server Insecure",
tooltip:
'Allow plain text connection to AD/LDAP server, defaults to "off"',
type: "on|off"
type: "on|off",
},
{
name: "comment",
@@ -309,28 +309,28 @@ export const fieldsConfigurations: any = {
label: "Comment",
tooltip: "Optionally add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
policy_opa: [
{
name: "url",
required: true,
label: "OPA URL",
type: "string"
type: "string",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string"
type: "string",
},
{
name: "policy_opa",
required: true,
label: "Policy OPA",
type: "string"
}
type: "string",
},
],
kms_vault: [],
kms_kes: [],
@@ -339,29 +339,29 @@ export const fieldsConfigurations: any = {
name: "endpoint",
required: true,
label: "Endpoint",
type: "string"
type: "string",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string"
}
type: "string",
},
],
audit_webhook: [
{
name: "endpoint",
required: true,
label: "Endpoint",
type: "string"
type: "string",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string"
}
]
type: "string",
},
],
};
const commonFields = [
@@ -371,7 +371,7 @@ const commonFields = [
required: true,
tooltip: "staging dir for undelivered messages e.g. '/home/events'",
type: "string"
type: "string",
},
{
name: "queue-limit",
@@ -379,15 +379,15 @@ const commonFields = [
required: false,
tooltip: "maximum limit for undelivered messages, defaults to '10000'",
type: "number"
type: "number",
},
{
name: "comment",
label: "Comment",
required: false,
type: "string",
multiline: true
}
multiline: true,
},
];
export const notificationEndpointsFields: any = {
@@ -398,77 +398,77 @@ export const notificationEndpointsFields: any = {
required: true,
tooltip: "comma separated list of Kafka broker addresses",
type: "string"
type: "string",
},
{
name: "topic",
label: "Topic",
tooltip: "Kafka topic used for bucket notifications",
type: "string"
type: "string",
},
{
name: "sasl_username",
label: "SASL Username",
tooltip: "username for SASL/PLAIN or SASL/SCRAM authentication",
type: "string"
type: "string",
},
{
name: "sasl_password",
label: "SASL Password",
tooltip: "password for SASL/PLAIN or SASL/SCRAM authentication",
type: "string"
type: "string",
},
{
name: "sasl_mechanism",
label: "SASL Mechanism",
tooltip: "sasl authentication mechanism, default 'PLAIN'",
type: "string"
type: "string",
},
{
name: "tls_client_auth",
label: "TLS Client Auth",
tooltip:
"clientAuth determines the Kafka server's policy for TLS client auth",
type: "string"
type: "string",
},
{
name: "sasl",
label: "SASL",
tooltip: "set to 'on' to enable SASL authentication",
type: "on|off"
type: "on|off",
},
{
name: "tls",
label: "TLS",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
type: "on|off",
},
{
name: "tls_skip_verify",
label: "TLS skip verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
type: "on|off",
},
{
name: "client_tls_cert",
label: "client TLS cert",
tooltip: "path to client certificate for mTLS auth",
type: "path"
type: "path",
},
{
name: "client_tls_key",
label: "client TLS key",
tooltip: "path to client key for mTLS auth",
type: "path"
type: "path",
},
{
name: "version",
label: "Version",
tooltip: "specify the version of the Kafka cluster e.g '2.2.0'",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyAmqp]: [
{
@@ -477,68 +477,68 @@ export const notificationEndpointsFields: any = {
label: "url",
tooltip:
"AMQP server endpoint e.g. `amqp://myuser:mypassword@localhost:5672`",
type: "url"
type: "url",
},
{
name: "exchange",
label: "exchange",
tooltip: "name of the AMQP exchange",
type: "string"
type: "string",
},
{
name: "exchange_type",
label: "exchange_type",
tooltip: "AMQP exchange type",
type: "string"
type: "string",
},
{
name: "routing_key",
label: "routing_key",
tooltip: "routing key for publishing",
type: "string"
type: "string",
},
{
name: "mandatory",
label: "mandatory",
tooltip:
"quietly ignore undelivered messages when set to 'off', default is 'on'",
type: "on|off"
type: "on|off",
},
{
name: "durable",
label: "durable",
tooltip:
"persist queue across broker restarts when set to 'on', default is 'off'",
type: "on|off"
type: "on|off",
},
{
name: "no_wait",
label: "no_wait",
tooltip:
"non-blocking message delivery when set to 'on', default is 'off'",
type: "on|off"
type: "on|off",
},
{
name: "internal",
label: "internal",
tooltip:
"set to 'on' for exchange to be not used directly by publishers, but only when bound to other exchanges",
type: "on|off"
type: "on|off",
},
{
name: "auto_deleted",
label: "auto_deleted",
tooltip:
"auto delete queue when set to 'on', when there are no consumers",
type: "on|off"
type: "on|off",
},
{
name: "delivery_mode",
label: "delivery_mode",
tooltip: "set to '1' for non-persistent or '2' for persistent queue",
type: "number"
type: "number",
},
...commonFields
...commonFields,
],
[notifyRedis]: [
{
@@ -546,22 +546,22 @@ export const notificationEndpointsFields: any = {
required: true,
label: "address",
tooltip: "Redis server's address. For example: `localhost:6379`",
type: "address"
type: "address",
},
{
name: "key",
required: true,
label: "key",
tooltip: "Redis key to store/update events, key is auto-created",
type: "string"
type: "string",
},
{
name: "password",
label: "password",
tooltip: "Redis server password",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyMqtt]: [
{
@@ -569,46 +569,46 @@ export const notificationEndpointsFields: any = {
required: true,
label: "broker",
tooltip: "MQTT server endpoint e.g. `tcp://localhost:1883`",
type: "uri"
type: "uri",
},
{
name: "topic",
required: true,
label: "topic",
tooltip: "name of the MQTT topic to publish",
type: "string"
type: "string",
},
{
name: "username",
label: "username",
tooltip: "MQTT username",
type: "string"
type: "string",
},
{
name: "password",
label: "password",
tooltip: "MQTT password",
type: "string"
type: "string",
},
{
name: "qos",
label: "qos",
tooltip: "set the quality of service priority, defaults to '0'",
type: "number"
type: "number",
},
{
name: "keep_alive_interval",
label: "keep_alive_interval",
tooltip: "keep-alive interval for MQTT connections in s,m,h,d",
type: "duration"
type: "duration",
},
{
name: "reconnect_interval",
label: "reconnect_interval",
tooltip: "reconnect interval for MQTT connections in s,m,h,d",
type: "duration"
type: "duration",
},
...commonFields
...commonFields,
],
[notifyNats]: [
{
@@ -616,95 +616,95 @@ export const notificationEndpointsFields: any = {
required: true,
label: "address",
tooltip: "NATS server address e.g. '0.0.0.0:4222'",
type: "address"
type: "address",
},
{
name: "subject",
required: true,
label: "subject",
tooltip: "NATS subscription subject",
type: "string"
type: "string",
},
{
name: "username",
label: "username",
tooltip: "NATS username",
type: "string"
type: "string",
},
{
name: "password",
label: "password",
tooltip: "NATS password",
type: "string"
type: "string",
},
{
name: "token",
label: "token",
tooltip: "NATS token",
type: "string"
type: "string",
},
{
name: "tls",
label: "tls",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
type: "on|off",
},
{
name: "tls_skip_verify",
label: "tls_skip_verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
type: "on|off",
},
{
name: "ping_interval",
label: "ping_interval",
tooltip: "client ping commands interval in s,m,h,d. Disabled by default",
type: "duration"
type: "duration",
},
{
name: "streaming",
label: "streaming",
tooltip: "set to 'on', to use streaming NATS server",
type: "on|off"
type: "on|off",
},
{
name: "streaming_async",
label: "streaming_async",
tooltip: "set to 'on', to enable asynchronous publish",
type: "on|off"
type: "on|off",
},
{
name: "streaming_max_pub_acks_in_flight",
label: "streaming_max_pub_acks_in_flight",
tooltip: "number of messages to publish without waiting for ACKs",
type: "number"
type: "number",
},
{
name: "streaming_cluster_id",
label: "streaming_cluster_id",
tooltip: "unique ID for NATS streaming cluster",
type: "string"
type: "string",
},
{
name: "cert_authority",
label: "cert_authority",
tooltip: "path to certificate chain of the target NATS server",
type: "string"
type: "string",
},
{
name: "client_cert",
label: "client_cert",
tooltip: "client cert for NATS mTLS auth",
type: "string"
type: "string",
},
{
name: "client_key",
label: "client_key",
tooltip: "client cert key for NATS mTLS auth",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyElasticsearch]: [
{
@@ -713,7 +713,7 @@ export const notificationEndpointsFields: any = {
label: "url",
tooltip:
"Elasticsearch server's address, with optional authentication info",
type: "url"
type: "url",
},
{
name: "index",
@@ -721,7 +721,7 @@ export const notificationEndpointsFields: any = {
label: "index",
tooltip:
"Elasticsearch index to store/update events, index is auto-created",
type: "string"
type: "string",
},
{
name: "format",
@@ -729,9 +729,9 @@ export const notificationEndpointsFields: any = {
label: "format",
tooltip:
"'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'",
type: "enum"
type: "enum",
},
...commonFields
...commonFields,
],
[notifyWebhooks]: [
{
@@ -740,15 +740,15 @@ export const notificationEndpointsFields: any = {
label: "endpoint",
tooltip:
"webhook server endpoint e.g. http://localhost:8080/minio/events",
type: "url"
type: "url",
},
{
name: "auth_token",
label: "auth_token",
tooltip: "opaque string or JWT authorization token",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyNsq]: [
{
@@ -756,34 +756,34 @@ export const notificationEndpointsFields: any = {
required: true,
label: "nsqd_address",
tooltip: "NSQ server address e.g. '127.0.0.1:4150'",
type: "address"
type: "address",
},
{
name: "topic",
required: true,
label: "topic",
tooltip: "NSQ topic",
type: "string"
type: "string",
},
{
name: "tls",
label: "tls",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
type: "on|off",
},
{
name: "tls_skip_verify",
label: "tls_skip_verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
type: "on|off",
},
...commonFields
]
...commonFields,
],
};
export const removeEmptyFields = (formFields: IElementValue[]) => {
const nonEmptyFields = formFields.filter(field => field.value !== "");
const nonEmptyFields = formFields.filter((field) => field.value !== "");
return nonEmptyFields;
};

View File

@@ -14,7 +14,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, { useEffect } from "react";
import clsx from "clsx";
import {
createStyles,
@@ -28,7 +28,6 @@ import Box from "@material-ui/core/Box";
import Typography from "@material-ui/core/Typography";
import Container from "@material-ui/core/Container";
import Link from "@material-ui/core/Link";
import history from "../../history";
import {
Redirect,
@@ -50,7 +49,7 @@ import Buckets from "./Buckets/Buckets";
import Policies from "./Policies/Policies";
import Permissions from "./Permissions/Permissions";
import Dashboard from "./Dashboard/Dashboard";
import Menu from "./Menu";
import Menu from "./Menu/Menu";
import api from "../../common/api";
import storage from "local-storage-fallback";
import NotFoundPage from "../NotFoundPage";
@@ -63,7 +62,12 @@ import { Button, LinearProgress } from "@material-ui/core";
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
import Trace from "./Trace/Trace";
import Logs from "./Logs/Logs";
import Heal from "./Heal/Heal";
import Watch from "./Watch/Watch";
import ListTenants from "./Tenants/ListTenants/ListTenants";
import { ISessionResponse } from "./types";
import { saveSessionResponse } from "./actions";
import TenantDetails from "./Tenants/TenantDetails/TenantDetails";
function Copyright() {
return (
@@ -129,6 +133,7 @@ const styles = (theme: Theme) =>
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
overflowX: "hidden",
},
drawerPaperClose: {
overflowX: "hidden",
@@ -172,18 +177,6 @@ const styles = (theme: Theme) =>
},
});
const mapState = (state: AppState) => ({
open: state.system.sidebarOpen,
needsRestart: state.system.serverNeedsRestart,
isServerLoading: state.system.serverIsLoading,
});
const connector = connect(mapState, {
setMenuOpen,
serverNeedsRestart,
serverIsLoading,
});
interface IConsoleProps {
open: boolean;
needsRestart: boolean;
@@ -193,143 +186,218 @@ interface IConsoleProps {
setMenuOpen: typeof setMenuOpen;
serverNeedsRestart: typeof serverNeedsRestart;
serverIsLoading: typeof serverIsLoading;
saveSessionResponse: typeof saveSessionResponse;
session: ISessionResponse;
}
class Console extends React.Component<
IConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps
> {
componentDidMount(): void {
const Console = ({
classes,
open,
needsRestart,
isServerLoading,
serverNeedsRestart,
serverIsLoading,
saveSessionResponse,
session,
}: IConsoleProps) => {
useEffect(() => {
api
.invoke("GET", `/api/v1/session`)
.then((res) => {
console.log(res);
saveSessionResponse(res);
})
.catch((err) => {
storage.removeItem("token");
history.push("/");
});
}
}, [saveSessionResponse]);
restartServer() {
this.props.serverIsLoading(true);
const restartServer = () => {
serverIsLoading(true);
api
.invoke("POST", "/api/v1/service/restart", {})
.then((res) => {
console.log("success restarting service");
console.log(res);
this.props.serverIsLoading(false);
this.props.serverNeedsRestart(false);
serverIsLoading(false);
serverNeedsRestart(false);
})
.catch((err) => {
this.props.serverIsLoading(false);
serverIsLoading(false);
console.log("failure restarting service");
console.log(err);
});
}
};
render() {
const { classes, open, needsRestart, isServerLoading } = this.props;
return (
<div className={classes.root}>
<CssBaseline />
<Drawer
variant="permanent"
classes={{
paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose),
}}
open={open}
>
{/*<div className={classes.toolbarIcon}>*/}
{/* <IconButton*/}
{/* onClick={() => {*/}
{/* this.props.setMenuOpen(false);*/}
{/* }}*/}
{/* >*/}
{/* <ChevronLeftIcon />*/}
{/* </IconButton>*/}
{/*</div>*/}
{/*<Divider />*/}
const allowedPages = session.pages.reduce(
(result: any, item: any, index: any) => {
result[item] = true;
return result;
},
{}
);
const routes = [
{
component: Dashboard,
path: "/dashboard",
},
{
component: Buckets,
path: "/buckets",
},
{
component: Buckets,
path: "/buckets/:bucketName",
},
{
component: Watch,
path: "/watch",
},
{
component: Users,
path: "/users",
},
{
component: Groups,
path: "/groups",
},
{
component: Policies,
path: "/policies",
},
{
component: Trace,
path: "/trace",
},
{
component: Logs,
path: "/logs",
},
{
component: Heal,
path: "/heal",
},
{
component: ListNotificationEndpoints,
path: "/notification-endpoints",
},
{
component: ConfigurationsList,
path: "/configurations-list",
},
{
component: Permissions,
path: "/permissions",
},
{
component: ServiceAccounts,
path: "/service-accounts",
},
{
component: WebhookPanel,
path: "/webhook/logger",
},
{
component: WebhookPanel,
path: "/webhook/audit",
},
{
component: ListTenants,
path: "/tenants",
},
{
component: TenantDetails,
path: "/clusters/:clusterName",
},
];
const allowedRoutes = routes.filter((route: any) => allowedPages[route.path]);
<Menu />
</Drawer>
return (
<React.Fragment>
{session.status == "ok" ? (
<div className={classes.root}>
<CssBaseline />
<Drawer
variant="permanent"
classes={{
paper: clsx(
classes.drawerPaper,
!open && classes.drawerPaperClose
),
}}
open={open}
>
<Menu pages={session.pages} />
</Drawer>
<main className={classes.content}>
{needsRestart && (
<div className={classes.warningBar}>
{isServerLoading ? (
<React.Fragment>
The server is restarting.
<LinearProgress />
</React.Fragment>
) : (
<main className={classes.content}>
{needsRestart && (
<div className={classes.warningBar}>
{isServerLoading ? (
<React.Fragment>
The server is restarting.
<LinearProgress />
</React.Fragment>
) : (
<React.Fragment>
The instance needs to be restarted for configuration changes
to take effect.{" "}
to take effect.{" "}
<Button
color="secondary"
size="small"
onClick={() => {
this.restartServer();
restartServer();
}}
>
Restart
</Button>
</Button>
</React.Fragment>
)}
</div>
)}
<div className={classes.appBarSpacer} />
<Container maxWidth="lg" className={classes.container}>
<Router history={history}>
<Switch>
<Route path="/buckets" component={Buckets} />
<Route exact path="/permissions" component={Permissions} />
<Route exact path="/policies" component={Policies} />
<Route
exact
path="/service_accounts"
component={ServiceAccounts}
/>
<Route exact path="/users" component={Users} />
<Route exact path="/dashboard" component={Dashboard} />
<Route exct path="/groups" component={Groups} />
<Route
exact
path="/notification-endpoints"
component={ListNotificationEndpoints}
/>
<Route
exact
path="/configurations-list"
component={ConfigurationsList}
/>
<Route exact path="/webhook/logger" component={WebhookPanel} />
<Route exact path="/webhook/audit" component={WebhookPanel} />
<Route exct path="/trace" component={Trace} />
<Route exct path="/logs" component={Logs} />
<Route exct path="/watch" component={Watch} />
<Route exact path="/">
<Redirect to="/dashboard" />
</Route>
<Route component={NotFoundPage} />
</Switch>
</Router>
</div>
)}
<div className={classes.appBarSpacer} />
<Container maxWidth="lg" className={classes.container}>
<Router history={history}>
<Switch>
{allowedRoutes.map((route: any) => (
<Route
key={route.path}
exact
path={route.path}
component={route.component}
/>
))}
{allowedRoutes.length > 0 ? (
<Route exact path="/">
<Redirect to={allowedRoutes[0].path} />
</Route>
) : null}
</Switch>
</Router>
<Box pt={4}>
<Copyright />
</Box>
</Container>
</main>
</div>
);
}
}
<Box pt={4}>
<Copyright />
</Box>
</Container>
</main>
</div>
) : null}
</React.Fragment>
);
};
// );
const mapState = (state: AppState) => ({
open: state.system.sidebarOpen,
needsRestart: state.system.serverNeedsRestart,
isServerLoading: state.system.serverIsLoading,
session: state.console.session,
});
export default withRouter(connector(withStyles(styles)(Console)));
// export default withStyles(styles)(connector(Console));
// export default compose(
// withStyles(styles),
// connector
// )(withRouter(Console))
const connector = connect(mapState, {
setMenuOpen,
serverNeedsRestart,
serverIsLoading,
saveSessionResponse,
});
export default connector(withStyles(styles)(Console));

View File

@@ -26,10 +26,11 @@ import { CreateIcon } from "../../../icons";
import api from "../../../common/api";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import { GroupsList } from "./types";
import { groupsSort } from "../../../utils/sortFunctions";
import { stringSort } from "../../../utils/sortFunctions";
import AddGroup from "../Groups/AddGroup";
import DeleteGroup from "./DeleteGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import SetPolicy from "../Policies/SetPolicy";
interface IGroupsProps {
classes: any;
@@ -39,50 +40,50 @@ interface IGroupsProps {
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
}
boxShadow: "0px 3px 6px #00000012",
},
});
const Groups = ({ classes }: IGroupsProps) => {
@@ -96,6 +97,7 @@ const Groups = ({ classes }: IGroupsProps) => {
const [page, setPage] = useState<number>(0);
const [error, setError] = useState<string>("");
const [filter, setFilter] = useState<string>("");
const [policyOpen, setPolicyOpen] = useState<boolean>(false);
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
@@ -126,7 +128,7 @@ const Groups = ({ classes }: IGroupsProps) => {
.then((res: GroupsList) => {
let resGroups: string[] = [];
if (res.groups !== null) {
resGroups = res.groups.sort(groupsSort);
resGroups = res.groups.sort(stringSort);
}
setRecords(resGroups);
const total = !res.total ? 0 : res.total;
@@ -140,7 +142,7 @@ const Groups = ({ classes }: IGroupsProps) => {
setPage(newPage);
}
})
.catch(err => {
.catch((err) => {
setError(err);
isLoading(false);
});
@@ -162,7 +164,7 @@ const Groups = ({ classes }: IGroupsProps) => {
}
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.includes(filter)
);
@@ -176,9 +178,15 @@ const Groups = ({ classes }: IGroupsProps) => {
setSelectedGroup(group);
};
const setPolicyAction = (selectionElement: any): void => {
setPolicyOpen(true);
setSelectedGroup(selectionElement);
};
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "delete", onClick: deleteAction }
{ type: "description", onClick: setPolicyAction },
{ type: "delete", onClick: deleteAction },
];
return (
@@ -197,6 +205,16 @@ const Groups = ({ classes }: IGroupsProps) => {
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
{setPolicyOpen && (
<SetPolicy
open={policyOpen}
selectedGroup={selectedGroup}
selectedUser={null}
closeModalAndRefresh={() => {
setPolicyOpen(false);
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Groups</Typography>
@@ -217,9 +235,9 @@ const Groups = ({ classes }: IGroupsProps) => {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
onChange={(e) => {
setFilter(e.target.value);
}}
/>
@@ -255,11 +273,11 @@ const Groups = ({ classes }: IGroupsProps) => {
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>

View File

@@ -38,43 +38,43 @@ interface IGroupsProps {
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "left",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
filterField: {
background: "#FFFFFF",
@@ -82,24 +82,24 @@ const styles = (theme: Theme) =>
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: "100%",
zIndex: 500
zIndex: 500,
},
noFound: {
textAlign: "center",
padding: "10px 0"
padding: "10px 0",
},
tableContainer: {
maxHeight: 250
maxHeight: 250,
},
stickyHeader: {
backgroundColor: "#fff"
}
backgroundColor: "#fff",
},
});
const UsersSelectors = ({
classes,
selectedUsers,
setSelectedUsers
setSelectedUsers,
}: IGroupsProps) => {
//Local States
const [records, setRecords] = useState<any[]>([]);
@@ -133,7 +133,7 @@ const UsersSelectors = ({
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
elements = elements.filter((element) => element !== value);
}
setSelectedUsers(elements);
@@ -154,13 +154,13 @@ const UsersSelectors = ({
setError("");
isLoading(false);
})
.catch(err => {
.catch((err) => {
setError(err);
isLoading(false);
});
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.accessKey.includes(filter)
);
@@ -185,9 +185,9 @@ const UsersSelectors = ({
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
onChange={(e) => {
setFilter(e.target.value);
}}
/>

View File

@@ -0,0 +1,327 @@
import { HorizontalBar } from "react-chartjs-2";
import React, { useEffect, useState } from "react";
import {
Button,
Grid,
Typography,
TextField,
Checkbox,
} from "@material-ui/core";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { wsProtocol } from "../../../utils/wsUtils";
import api from "../../../common/api";
import { FormControl, MenuItem, Select } from "@material-ui/core";
import { BucketList, Bucket } from "../Watch/types";
import { HealStatus, colorH } from "./types";
import { niceBytes } from "../../../common/utils";
const styles = (theme: Theme) =>
createStyles({
watchList: {
background: "white",
maxHeight: "400",
overflow: "auto",
"& ul": {
margin: "4",
padding: "0",
},
"& ul li": {
listStyle: "none",
margin: "0",
padding: "0",
borderBottom: "1px solid #dedede",
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
inputField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
marginLeft: 10,
boxShadow: "0px 3px 6px #00000012",
},
fieldContainer: {
background: "#FFFFFF",
padding: 0,
borderRadius: 5,
marginLeft: 10,
textAlign: "left",
minWidth: "206",
boxShadow: "0px 3px 6px #00000012",
},
lastElementWPadding: {
paddingRight: "78",
},
});
interface IHeal {
classes: any;
}
const Heal = ({ classes }: IHeal) => {
const [start, setStart] = useState(false);
const [bucketName, setBucketName] = useState("Select Bucket");
const [bucketList, setBucketList] = useState<Bucket[]>([]);
const [prefix, setPrefix] = useState("");
const [recursive, setRecursive] = useState(false);
const [forceStart, setForceStart] = useState(false);
const [forceStop, setForceStop] = useState(false);
// healStatus states
const [hStatus, setHStatus] = useState({
beforeHeal: [0, 0, 0, 0],
afterHeal: [0, 0, 0, 0],
objectsHealed: 0,
objectsScanned: 0,
healDuration: 0,
sizeScanned: "",
});
const fetchBucketList = () => {
api
.invoke("GET", `/api/v1/buckets`)
.then((res: BucketList) => {
let buckets: Bucket[] = [];
if (res.buckets !== null) {
buckets = res.buckets;
}
setBucketList(buckets);
})
.catch((err: any) => {
console.log(err);
});
};
useEffect(() => {
fetchBucketList();
}, []);
// forceStart and forceStop need to be mutually exclusive
useEffect(() => {
if (forceStart === true) {
setForceStop(false);
}
}, [forceStart]);
useEffect(() => {
if (forceStop === true) {
setForceStart(false);
}
}, [forceStop]);
const colorHealthArr = (color: colorH) => {
return [color.Green, color.Yellow, color.Red, color.Grey];
};
useEffect(() => {
// begin watch if bucketName in bucketList and start pressed
if (start) {
// values stored here to update chart
const cB: colorH = { Green: 0, Yellow: 0, Red: 0, Grey: 0 };
const cA: colorH = { Green: 0, Yellow: 0, Red: 0, Grey: 0 };
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket(
`${wsProt}://${url.hostname}:${port}/ws/heal/${bucketName}?prefix=${prefix}&recursive=${recursive}&force-start=${forceStart}&force-stop=${forceStop}`
);
if (c !== null) {
c.onopen = () => {
console.log("WebSocket Client Connected");
c.send("ok");
};
c.onmessage = (message: IMessageEvent) => {
let m: HealStatus = JSON.parse(message.data.toString());
// Store percentage per health color
for (const [key, value] of Object.entries(m.healthAfterCols)) {
cA[key] = (value * 100) / m.itemsScanned;
}
for (const [key, value] of Object.entries(m.healthBeforeCols)) {
cB[key] = (value * 100) / m.itemsScanned;
}
setHStatus({
beforeHeal: colorHealthArr(cB),
afterHeal: colorHealthArr(cA),
objectsHealed: m.objectsHealed,
objectsScanned: m.objectsScanned,
healDuration: m.healDuration,
sizeScanned: niceBytes(m.bytesScanned.toString()),
});
};
c.onclose = () => {
setStart(false);
console.log("connection closed by server");
};
return () => {
// close websocket on useEffect cleanup
c.close(1000);
console.log("closing websockets");
};
}
}
}, [start]);
let data = {
labels: ["Green", "Yellow", "Red", "Grey"],
datasets: [
{
label: "After Healing",
data: hStatus.afterHeal,
backgroundColor: "rgba(0, 0, 255, 0.2)",
borderColor: "rgba(54, 162, 235, 1)",
borderWidth: 1,
},
{
label: "Before Healing",
data: hStatus.beforeHeal,
backgroundColor: "rgba(153, 102, 255, 0.2)",
borderColor: "rgba(153, 102, 255, 1)",
borderWidth: 1,
},
],
};
const bucketNames = bucketList.map((bucketName) => ({
label: bucketName.name,
value: bucketName.name,
}));
return (
<React.Fragment>
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Heal</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<FormControl variant="outlined">
<Select
id="bucket-name"
name="bucket-name"
value={bucketName}
onChange={(e) => {
setBucketName(e.target.value as string);
}}
className={classes.fieldContainer}
disabled={false}
>
<MenuItem value="" key={`select-bucket-name-default`}>
Select Bucket
</MenuItem>
{bucketNames.map((option) => (
<MenuItem
value={option.value}
key={`select-bucket-name-${option.label}`}
>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
placeholder="Prefix"
className={classes.inputField}
id="prefix-resource"
label=""
disabled={false}
InputProps={{
disableUnderline: true,
}}
onChange={(e) => {
setPrefix(e.target.value);
}}
/>
<Button
type="submit"
variant="contained"
color="primary"
disabled={start}
onClick={() => setStart(true)}
>
Start
</Button>
<Grid item xs={12}>
<span>{"Recursive"}</span>
<Checkbox
name="recursive"
id="recursive"
value="recursive"
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={recursive}
onChange={(e) => {
setRecursive(e.target.checked);
}}
disabled={false}
/>
<span>{"Force Start"}</span>
<Checkbox
name="recursive"
id="recursive"
value="recursive"
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={forceStart}
onChange={(e) => {
setForceStart(e.target.checked);
}}
disabled={false}
/>
<span>{"Force Stop"}</span>
<Checkbox
name="recursive"
id="recursive"
value="recursive"
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={forceStop}
onChange={(e) => {
setForceStop(e.target.checked);
}}
disabled={false}
/>
<span className={classes.lastElementWPadding}>{""}</span>
</Grid>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<HorizontalBar
data={data}
width={100}
height={30}
options={{
title: {
display: true,
text: "Item's Health Status [%]",
fontSize: 20,
},
legend: {
display: true,
position: "right",
},
}}
/>
<Grid item xs={12}>
<br />
Size scanned: {hStatus.sizeScanned}
<br />
Objects healed: {hStatus.objectsHealed} / {hStatus.objectsScanned}
<br />
Healing time: {hStatus.healDuration}s
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(Heal);

View File

@@ -0,0 +1,64 @@
// 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 HealDriveInfo {
uuid: string;
endpoint: string;
state: string;
}
export interface MomentHealth {
color: string;
offline: number;
online: number;
missing: number;
corrupted: number;
drives: HealDriveInfo[];
}
export interface HealItemStatus {
status: string;
error: string;
type: string;
name: string;
before: MomentHealth;
after: MomentHealth;
size: number;
}
export interface HealStatus {
healDuration: number;
bytesScanned: number;
objectsScanned: number;
itemsScanned: number;
// Counters for healed objects and all kinds of healed items
objectsHealed: number;
itemsHealed: number;
itemsHealthStatus: HealItemStatus[];
// Map of health color code to number of objects with that
// health color code.
healthBeforeCols: Map<string, number>;
healthAfterCols: Map<string, number>;
}
// colorH used to save health's percentage per color
export interface colorH {
[Green: string]: number;
Yellow: number;
Red: number;
Grey: number;
}

View File

@@ -33,12 +33,12 @@ export type LogActionTypes = LogMessageReceivedAction | LogResetMessagesAction;
export function logMessageReceived(message: LogMessage) {
return {
type: LOG_MESSAGE_RECEIVED,
message: message
message: message,
};
}
export function logResetMessages() {
return {
type: LOG_RESET_MESSAGES
type: LOG_RESET_MESSAGES,
};
}

View File

@@ -17,7 +17,7 @@
import {
LOG_MESSAGE_RECEIVED,
LOG_RESET_MESSAGES,
LogActionTypes
LogActionTypes,
} from "./actions";
import { LogMessage } from "./types";
@@ -26,7 +26,7 @@ export interface LogState {
}
const initialState: LogState = {
messages: []
messages: [],
};
export function logReducer(
@@ -37,12 +37,12 @@ export function logReducer(
case LOG_MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message]
messages: [...state.messages, action.message],
};
case LOG_RESET_MESSAGES:
return {
...state,
messages: []
messages: [],
};
default:
return state;

View File

@@ -1,205 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 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 ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import WebAssetIcon from "@material-ui/icons/WebAsset";
import CenterFocusWeakIcon from '@material-ui/icons/CenterFocusWeak';
import ListItemText from "@material-ui/core/ListItemText";
import { NavLink } from "react-router-dom";
import { Divider, Typography, withStyles } from "@material-ui/core";
import { ExitToApp } from "@material-ui/icons";
import { AppState } from "../../store";
import { connect } from "react-redux";
import { userLoggedIn } from "../../actions";
import List from "@material-ui/core/List";
import storage from "local-storage-fallback";
import history from "../../history";
import logo from "../../icons/minio_console_logo.svg";
import {
BucketsIcon,
DashboardIcon,
PermissionIcon,
UsersIcon,
} from "../../icons";
import { createStyles, Theme } from "@material-ui/core/styles";
import PersonIcon from "@material-ui/icons/Person";
import api from "../../common/api";
import NotificationsIcon from "@material-ui/icons/Notifications";
import ListAltIcon from "@material-ui/icons/ListAlt";
import LoopIcon from "@material-ui/icons/Loop";
const styles = (theme: Theme) =>
createStyles({
logo: {
paddingTop: "42px",
marginBottom: "20px",
textAlign: "center",
"& img": {
width: "120px",
},
},
menuList: {
"& .active": {
borderTopLeftRadius: "3px",
borderBottomLeftRadius: "3px",
color: "white",
background:
"transparent linear-gradient(90deg, #362585 0%, #362585 7%, #281B6F 39%, #1F1661 100%) 0% 0% no-repeat padding-box",
"& .MuiSvgIcon-root": {
color: "white",
},
},
"& .MuiListItem-root": {
marginTop: "16px",
},
paddingLeft: "30px",
"& .MuiSvgIcon-root": {
fontSize: "18px",
color: "#393939",
},
"& .MuiListItemIcon-root": {
minWidth: "40px",
},
"& .MuiTypography-root": {
fontSize: "14px",
},
"& .MuiListItem-gutters": {
paddingRight: "0px",
},
},
});
const mapState = (state: AppState) => ({
open: state.system.loggedIn,
});
const connector = connect(mapState, { userLoggedIn });
interface MenuProps {
classes: any;
userLoggedIn: typeof userLoggedIn;
}
class Menu extends React.Component<MenuProps> {
logout() {
const deleteSession = () => {
storage.removeItem("token");
this.props.userLoggedIn(false);
history.push("/");
};
api
.invoke("POST", `/api/v1/logout`)
.then(() => {
deleteSession();
})
.catch((err: any) => {
console.log(err);
deleteSession();
});
}
render() {
const { classes } = this.props;
return (
<React.Fragment>
<div className={classes.logo}>
<img src={logo} alt="logo" />
</div>
<List className={classes.menuList}>
<ListItem button component={NavLink} to="/dashboard">
<ListItemIcon>
<DashboardIcon />
</ListItemIcon>
<ListItemText primary="Dashboard" />
</ListItem>
<ListItem button component={NavLink} to="/buckets">
<ListItemIcon>
<BucketsIcon />
</ListItemIcon>
<ListItemText primary="Buckets" />
</ListItem>
<ListItem button component={NavLink} to="/watch">
<ListItemIcon>
<CenterFocusWeakIcon />
</ListItemIcon>
<ListItemText primary="Watch" />
</ListItem>
<Divider />
<ListItem component={Typography}>Admin</ListItem>
<ListItem button component={NavLink} to="/users">
<ListItemIcon>
<PersonIcon />
</ListItemIcon>
<ListItemText primary="Users" />
</ListItem>
<ListItem button component={NavLink} to="/groups">
<ListItemIcon>
<UsersIcon />
</ListItemIcon>
<ListItemText primary="Groups" />
</ListItem>
<ListItem button component={NavLink} to="/policies">
<ListItemIcon>
<PermissionIcon />
</ListItemIcon>
<ListItemText primary="IAM Policies" />
</ListItem>
<ListItem button component={NavLink} to="/trace">
<ListItemIcon>
<LoopIcon />
</ListItemIcon>
<ListItemText primary="Trace" />
</ListItem>
<ListItem button component={NavLink} to="/logs">
<ListItemIcon>
<WebAssetIcon />
</ListItemIcon>
<ListItemText primary="Console Logs" />
</ListItem>
<ListItem component={Typography}>Configuration</ListItem>
<ListItem button component={NavLink} to="/notification-endpoints">
<ListItemIcon>
<NotificationsIcon />
</ListItemIcon>
<ListItemText primary="Lambda Notifications" />
</ListItem>
<ListItem button component={NavLink} to="/configurations-list">
<ListItemIcon>
<ListAltIcon />
</ListItemIcon>
<ListItemText primary="Configurations List" />
</ListItem>
<Divider />
<ListItem
button
onClick={() => {
this.logout();
}}
>
<ListItemIcon>
<ExitToApp />
</ListItemIcon>
<ListItemText primary="Logout" />
</ListItem>
</List>
</React.Fragment>
);
}
}
export default connector(withStyles(styles)(Menu));

View File

@@ -0,0 +1,424 @@
// 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, { useState } from "react";
import { connect } from "react-redux";
import { NavLink } from "react-router-dom";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import WebAssetIcon from "@material-ui/icons/WebAsset";
import HealingIcon from "@material-ui/icons/Healing";
import Collapse from "@material-ui/core/Collapse";
import ListItemText from "@material-ui/core/ListItemText";
import List from "@material-ui/core/List";
import { Divider, Typography, withStyles } from "@material-ui/core";
import { ExitToApp } from "@material-ui/icons";
import storage from "local-storage-fallback";
import { createStyles, Theme } from "@material-ui/core/styles";
import history from "../../../history";
import logo from "../../../icons/minio_console_logo.svg";
import { AppState } from "../../../store";
import { userLoggedIn } from "../../../actions";
import api from "../../../common/api";
import WatchIcon from "../../../icons/WatchIcon";
import { menuGroups } from "./utils";
import { IMenuProps } from "./types";
import {
BucketsIcon,
ClustersIcon,
ConfigurationsListIcon,
DashboardIcon,
GroupsIcon,
IAMPoliciesIcon,
LambdaNotificationsIcon,
MirroringIcon,
ServiceAccountsIcon,
TraceIcon,
UsersIcon,
WarpIcon,
} from "../../../icons";
const styles = (theme: Theme) =>
createStyles({
logo: {
paddingTop: "42px",
marginBottom: "20px",
textAlign: "center",
"& img": {
width: "120px",
},
},
menuList: {
"& .active": {
borderTopLeftRadius: "3px",
borderBottomLeftRadius: "3px",
color: "#fff",
background:
"transparent linear-gradient(90deg, #362585 0%, #362585 7%, #281B6F 39%, #1F1661 100%) 0% 0% no-repeat padding-box;",
boxShadow: "4px 4px 4px #A5A5A512",
fontWeight: 700,
"& .MuiSvgIcon-root": {
color: "white",
},
"& .MuiTypography-root": {
color: "#fff",
},
},
paddingLeft: "30px",
"& .MuiSvgIcon-root": {
fontSize: 16,
color: "#362585",
maxWidth: 14,
},
"& .MuiListItemIcon-root": {
minWidth: "25px",
},
"& .MuiTypography-root": {
fontSize: "12px",
color: "#2e2e2e",
},
"& .MuiListItem-gutters": {
paddingRight: 0,
},
},
extraMargin: {
"&.MuiListItem-gutters": {
marginLeft: 5,
},
},
groupTitle: {
color: "#220c7c",
fontSize: 10,
textTransform: "uppercase",
fontWeight: 700,
marginBottom: 3,
cursor: "pointer",
userSelect: "none",
},
subTitleMenu: {
fontWeight: 700,
marginLeft: 10,
"&.MuiTypography-root": {
fontSize: 13,
color: "#220c7c",
},
},
selectorArrow: {
marginLeft: 3,
marginTop: 1,
display: "inline-block",
width: 0,
height: 0,
borderStyle: "solid",
borderWidth: "3px 2.5px 0 2.5px",
borderColor: "#220C7C transparent transparent transparent",
transform: "rotateZ(0deg)",
transitionDuration: "0.2s",
},
selectorArrowOpen: {
transform: "rotateZ(180deg)",
},
});
const mapState = (state: AppState) => ({
open: state.system.loggedIn,
});
const connector = connect(mapState, { userLoggedIn });
// Menu State builder for groups
const menuStateBuilder = () => {
let elements: any = [];
menuGroups.forEach((menuItem) => {
if (menuItem.collapsible) {
elements[menuItem.group] = true;
}
});
return elements;
};
const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
const [menuOpen, setMenuOpen] = useState<any>(menuStateBuilder());
const logout = () => {
const deleteSession = () => {
storage.removeItem("token");
userLoggedIn(false);
history.push("/");
};
api
.invoke("POST", `/api/v1/logout`)
.then(() => {
deleteSession();
})
.catch((err: any) => {
console.log(err);
deleteSession();
});
};
const menuItems = [
{
group: "common",
type: "item",
component: NavLink,
to: "/dashboard",
name: "Dashboard",
icon: <DashboardIcon />,
},
{
group: "User",
type: "item",
component: NavLink,
to: "/buckets",
name: "Buckets",
icon: <BucketsIcon />,
},
{
group: "User",
type: "item",
component: NavLink,
to: "/service-accounts",
name: "Service Accounts",
icon: <ServiceAccountsIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/users",
name: "Users",
icon: <UsersIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/groups",
name: "Groups",
icon: <GroupsIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/policies",
name: "IAM Policies",
icon: <IAMPoliciesIcon />,
},
{
group: "Tools",
type: "item",
component: NavLink,
to: "/logs",
name: "Console Logs",
icon: <WebAssetIcon />,
},
{
group: "Tools",
type: "item",
component: NavLink,
to: "/watch",
name: "Watch",
icon: <WatchIcon />,
},
{
group: "Tools",
type: "item",
component: NavLink,
to: "/trace",
name: "Trace",
icon: <TraceIcon />,
},
{
group: "Tools",
type: "item",
component: NavLink,
to: "/heal",
name: "Heal",
icon: <HealingIcon />,
},
{
group: "Admin",
type: "title",
name: "Configurations",
component: Typography,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/notification-endpoints",
name: "Lambda Notifications",
icon: <LambdaNotificationsIcon />,
extraMargin: true,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/configurations-list",
name: "Configurations List",
icon: <ConfigurationsListIcon />,
extraMargin: true,
},
{
group: "Operator",
type: "item",
component: NavLink,
to: "/tenants",
name: "Tenants",
icon: <ClustersIcon />,
},
{
group: "Operator",
type: "item",
component: NavLink,
to: "/mirroring",
name: "Mirroring",
icon: <MirroringIcon />,
},
{
group: "Operator",
type: "item",
component: NavLink,
to: "/warp",
name: "Warp",
icon: <WarpIcon />,
},
];
const allowedPages = pages.reduce((result: any, item: any, index: any) => {
result[item] = true;
return result;
}, {});
const allowedItems = menuItems.filter(
(item: any) =>
allowedPages[item.to] || item.forceDisplay || item.type !== "item"
);
const setMenuCollapse = (menuClicked: string) => {
let newMenu: any = { ...menuOpen };
newMenu[menuClicked] = !newMenu[menuClicked];
setMenuOpen(newMenu);
};
return (
<React.Fragment>
<div className={classes.logo}>
<img src={logo} alt="logo" />
</div>
<List className={classes.menuList}>
{menuGroups.map((groupMember, index) => {
const filterByGroup = (allowedItems || []).filter(
(item: any) => item.group === groupMember.group
);
const countableElements = filterByGroup.filter(
(menuItem: any) => menuItem.type !== "title"
);
if (countableElements.length == 0) {
return null;
}
return (
<React.Fragment key={`menuElem-${index.toString()}`}>
{groupMember.label !== "" && (
<ListItem
className={classes.groupTitle}
onClick={() => {
if (groupMember.collapsible) {
setMenuCollapse(groupMember.group);
}
}}
>
{groupMember.label}
{groupMember.collapsible && (
<span
className={`${classes.selectorArrow} ${
menuOpen[groupMember.group]
? classes.selectorArrowOpen
: ""
}`}
/>
)}
</ListItem>
)}
<Collapse
in={
groupMember.collapsible ? menuOpen[groupMember.group] : true
}
timeout="auto"
unmountOnExit
key={`menuGroup-${groupMember.group}`}
>
{filterByGroup.map((page: any) => {
switch (page.type) {
case "item": {
return (
<ListItem
key={page.to}
button
component={page.component}
to={page.to}
className={
page.extraMargin ? classes.extraMargin : null
}
>
{page.icon && (
<ListItemIcon>{page.icon}</ListItemIcon>
)}
{page.name && <ListItemText primary={page.name} />}
</ListItem>
);
}
case "title": {
return (
<ListItem
key={page.name}
component={page.component}
className={classes.subTitleMenu}
>
{page.name}
</ListItem>
);
}
default:
}
})}
<Divider />
</Collapse>
</React.Fragment>
);
})}
<ListItem button onClick={logout}>
<ListItemIcon>
<ExitToApp />
</ListItemIcon>
<ListItemText primary="Logout" />
</ListItem>
</List>
</React.Fragment>
);
};
export default connector(withStyles(styles)(Menu));

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 { userLoggedIn } from "../../../actions";
export interface IMenuProps {
classes: any;
userLoggedIn: typeof userLoggedIn;
pages: 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/>.
export const menuGroups = [
{ label: "", group: "common", collapsible: false },
{ label: "User", group: "User", collapsible: true },
{ label: "Admin", group: "Admin", collapsible: true },
{ label: "Tools", group: "Tools", collapsible: true },
{ label: "Operator", group: "Operator", collapsible: true },
];

View File

@@ -31,14 +31,14 @@ import {
Radio,
RadioGroup,
Select,
TextField
TextField,
} from "@material-ui/core";
import {
createStyles,
lighten,
makeStyles,
Theme,
withStyles
withStyles,
} from "@material-ui/core/styles";
import api from "../../../common/api";
import clsx from "clsx";
@@ -61,21 +61,21 @@ const useToolbarStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1)
paddingRight: theme.spacing(1),
},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark
backgroundColor: theme.palette.secondary.dark,
},
title: {
flex: "1 1 100%"
}
flex: "1 1 100%",
},
})
);
@@ -90,7 +90,7 @@ const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
return (
<Toolbar
className={clsx(classes.root, {
[classes.highlight]: numSelected > 0
[classes.highlight]: numSelected > 0,
})}
>
{numSelected > 0 ? (
@@ -122,8 +122,8 @@ const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
});
interface IAddPermissionContentProps {
@@ -164,7 +164,7 @@ class AddPermissionContent extends React.Component<
resources: [],
buckets: [],
bucketsError: "",
loadingBuckets: false
loadingBuckets: false,
};
componentDidMount(): void {
@@ -176,10 +176,10 @@ class AddPermissionContent extends React.Component<
this.setState({
loadingBuckets: false,
buckets: res.buckets,
bucketsError: ""
bucketsError: "",
});
})
.catch(err => {
.catch((err) => {
this.setState({ loadingBuckets: false, bucketsError: err });
});
});
@@ -190,8 +190,8 @@ class AddPermissionContent extends React.Component<
name: selectedPermission.name,
description: selectedPermission.description,
effect: selectedPermission.effect,
resources: selectedPermission.resources.map(r => r.bucket_name),
action: selectedPermission.actions[0].type
resources: selectedPermission.resources.map((r) => r.bucket_name),
action: selectedPermission.actions[0].type,
});
}
}
@@ -204,7 +204,7 @@ class AddPermissionContent extends React.Component<
resources,
description,
effect,
action
action,
} = this.state;
const { selectedPermission } = this.props;
if (addLoading) {
@@ -219,23 +219,23 @@ class AddPermissionContent extends React.Component<
description: description,
effect: effect,
resources: resources,
actions: [action]
actions: [action],
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
} else {
@@ -245,23 +245,23 @@ class AddPermissionContent extends React.Component<
description: description,
effect: effect,
resources: resources,
actions: [action]
actions: [action],
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
}
@@ -280,14 +280,14 @@ class AddPermissionContent extends React.Component<
name,
description,
effect,
action
action,
} = this.state;
const handleSelectAllClick = (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (event.target.checked) {
const newSelecteds = buckets.map(n => n.name);
const newSelecteds = buckets.map((n) => n.name);
this.setState({ resources: newSelecteds });
return;
}
@@ -432,7 +432,7 @@ class AddPermissionContent extends React.Component<
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts"
"aria-label": "select all desserts",
}}
/>
</TableCell>
@@ -452,7 +452,9 @@ class AddPermissionContent extends React.Component<
return (
<TableRow
hover
onClick={event => handleClick(event, row.name)}
onClick={(event) =>
handleClick(event, row.name)
}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
@@ -500,7 +502,7 @@ class AddPermissionContent extends React.Component<
value={action}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
action: (event.target as HTMLInputElement).value
action: (event.target as HTMLInputElement).value,
});
}}
>

View File

@@ -0,0 +1,190 @@
// 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, { useCallback, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
Button,
LinearProgress,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import { User } from "../Users/types";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import { Policy, PolicyList } from "./types";
import api from "../../../common/api";
import { policySort } from "../../../utils/sortFunctions";
import { Group } from "../Groups/types";
interface ISetPolicyProps {
classes: any;
closeModalAndRefresh: () => void;
selectedUser: User | null;
selectedGroup: string | null;
open: boolean;
}
const styles = (theme: Theme) =>
createStyles({
...modalBasic,
buttonContainer: {
textAlign: "right",
},
});
const SetPolicy = ({
classes,
closeModalAndRefresh,
selectedUser,
selectedGroup,
open,
}: ISetPolicyProps) => {
//Local States
const [records, setRecords] = useState<Policy[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const fetchRecords = () => {
setLoading(true);
api
.invoke("GET", `/api/v1/policies?limit=1000`)
.then((res: PolicyList) => {
const policies = res.policies === null ? [] : res.policies;
setLoading(false);
setRecords(policies.sort(policySort));
setError("");
})
.catch((err) => {
setLoading(false);
setError(err);
});
};
const setPolicyAction = (policyName: string) => {
let entity = "user";
let value = null;
if (selectedGroup !== null) {
entity = "group";
value = selectedGroup;
} else {
if (selectedUser !== null) {
value = selectedUser.accessKey;
}
}
setLoading(true);
api
.invoke("PUT", `/api/v1/set-policy/${policyName}`, {
entityName: value,
entityType: entity,
})
.then((res: any) => {
setLoading(false);
setError("");
closeModalAndRefresh();
})
.catch((err) => {
setLoading(false);
setError(err);
});
};
useEffect(() => {
if (open) {
fetchRecords();
}
}, [open]);
return (
<ModalWrapper
onClose={() => {
closeModalAndRefresh();
}}
modalOpen={open}
title={
selectedUser !== null ? "Set Policy to User" : "Set Policy to Group"
}
>
<Grid container className={classes.formScrollable}>
<Grid item xs={12}>
<TableContainer component={Paper}>
<Table
className={classes.table}
size="small"
aria-label="a dense table"
>
<TableHead>
<TableRow>
<TableCell>Policy</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map((row) => (
<TableRow key={row.name}>
<TableCell component="th" scope="row">
{row.name}
</TableCell>
<TableCell align="right">
<Button
variant="contained"
color="primary"
size={"small"}
onClick={() => {
setPolicyAction(row.name);
}}
>
Set
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
onClick={() => {
closeModalAndRefresh();
}}
>
Cancel
</Button>
</Grid>
{loading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</ModalWrapper>
);
};
export default withStyles(styles)(SetPolicy);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@@ -14,363 +14,101 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import React, { useEffect, useState } from "react";
import Grid from "@material-ui/core/Grid";
import { UnControlled as CodeMirror } from "react-codemirror2";
import Typography from "@material-ui/core/Typography";
import { Button, FormControlLabel, LinearProgress } from "@material-ui/core";
import {
createStyles,
lighten,
makeStyles,
Theme,
withStyles
} from "@material-ui/core/styles";
import api from "../../../common/api";
import clsx from "clsx";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import Toolbar from "@material-ui/core/Toolbar";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";
import FilterListIcon from "@material-ui/icons/FilterList";
import { Permission, PermissionList } from "../Permissions/types";
import {
NewServiceAccount,
ServiceAccount,
ServiceAccountDetails
} from "./types";
import Switch from "@material-ui/core/Switch";
import { Button, LinearProgress, Tooltip } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import api from "../../../common/api";
import "codemirror/lib/codemirror.css";
import "codemirror/theme/material.css";
import { NewServiceAccount } from "./types";
import HelpIcon from "@material-ui/icons/Help";
const useToolbarStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1)
},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark
},
title: {
flex: "1 1 100%"
}
})
);
interface EnhancedTableToolbarProps {
numSelected: number;
}
const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
const classes = useToolbarStyles();
const { numSelected } = props;
return (
<Toolbar
className={clsx(classes.root, {
[classes.highlight]: numSelected > 0
})}
>
{numSelected > 0 ? (
<Typography
className={classes.title}
color="inherit"
variant="subtitle1"
>
{numSelected} selected
</Typography>
) : (
<Typography className={classes.title} variant="h6" id="tableTitle">
Permissions
</Typography>
)}
{numSelected > 0 ? (
<span />
) : (
<Tooltip title="Filter list">
<IconButton aria-label="filter list">
<FilterListIcon />
</IconButton>
</Tooltip>
)}
</Toolbar>
);
};
require("codemirror/mode/javascript/javascript");
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
jsonPolicyEditor: {
minHeight: 400,
width: "100%",
},
codeMirror: {
fontSize: 14,
},
buttonContainer: {
textAlign: "right",
},
...modalBasic,
});
interface IAddServiceAccountContentProps {
interface IAddServiceAccountProps {
classes: any;
open: boolean;
closeModalAndRefresh: (res: NewServiceAccount | null) => void;
selectedServiceAccount: ServiceAccount | null;
}
interface IAddServiceAccountContentState {
addLoading: boolean;
addError: string;
name: string;
enabled: boolean;
selectedPermissions: Permission[];
rowsPerPage: number;
page: number;
permissions: Permission[];
permissionsError: string;
loadingPermissions: boolean;
loadingServiceAccount: boolean;
}
const AddServiceAccount = ({
classes,
open,
closeModalAndRefresh,
}: IAddServiceAccountProps) => {
const [addSending, setAddSending] = useState(false);
const [addError, setAddError] = useState("");
const [policyDefinition, setPolicyDefinition] = useState("");
class AddServiceAccountContent extends React.Component<
IAddServiceAccountContentProps,
IAddServiceAccountContentState
> {
state: IAddServiceAccountContentState = {
addLoading: false,
addError: "",
name: "",
enabled: true,
selectedPermissions: [],
rowsPerPage: 5,
page: 0,
permissions: [],
permissionsError: "",
loadingPermissions: false,
loadingServiceAccount: false
useEffect(() => {
if (addSending) {
api
.invoke("POST", "/api/v1/service-accounts", {
policy: policyDefinition,
})
.then((res) => {
setAddSending(false);
setAddError("");
closeModalAndRefresh(res);
})
.catch((err) => {
setAddSending(false);
setAddError(err);
});
}
}, [
addSending,
setAddSending,
setAddError,
policyDefinition,
closeModalAndRefresh,
]);
const addServiceAccount = (e: React.FormEvent) => {
e.preventDefault();
setAddSending(true);
};
componentDidMount(): void {
// load a list of permissions
this.setState({ loadingPermissions: true }, () => {
api
.invoke("GET", `/api/v1/permissions?limit=1000`)
.then((res: PermissionList) => {
this.setState({
loadingPermissions: false,
permissions: res.permissions,
permissionsError: ""
});
})
.catch(err => {
this.setState({ loadingPermissions: false, permissionsError: err });
});
});
const { selectedServiceAccount } = this.props;
if (selectedServiceAccount !== null) {
this.setState({ loadingServiceAccount: true }, () => {
api
.invoke(
"GET",
`/api/v1/service_accounts/${selectedServiceAccount.id}`
)
.then((res: ServiceAccountDetails) => {
console.log(res);
this.setState({
loadingServiceAccount: false,
name: selectedServiceAccount.name,
enabled: selectedServiceAccount.enabled,
selectedPermissions:
res.permissions === undefined || res.permissions === null
? []
: res.permissions
});
})
.catch(err => {
this.setState({ loadingServiceAccount: false });
});
});
}
}
saveRecord(event: React.FormEvent) {
event.preventDefault();
const { name, addLoading, selectedPermissions, enabled } = this.state;
const { selectedServiceAccount } = this.props;
if (addLoading) {
return;
}
this.setState({ addLoading: true }, () => {
if (selectedServiceAccount !== null) {
api
.invoke(
"PUT",
`/api/v1/service_accounts/${selectedServiceAccount.id}`,
{
id: selectedServiceAccount.id,
name: name,
enabled: enabled,
permission_ids: selectedPermissions.map(p => p.id)
}
)
.then(res => {
this.setState(
{
addLoading: false,
addError: ""
},
() => {
this.props.closeModalAndRefresh(null);
}
);
})
.catch(err => {
this.setState({
addLoading: false,
addError: err
});
});
} else {
api
.invoke("POST", "/api/v1/service_accounts", {
name: name,
permission_ids: selectedPermissions.map(p => p.id)
})
.then((res: NewServiceAccount) => {
this.setState(
{
addLoading: false,
addError: ""
},
() => {
this.props.closeModalAndRefresh(res);
}
);
})
.catch(err => {
this.setState({
addLoading: false,
addError: err
});
});
}
});
}
render() {
const {
classes,
selectedServiceAccount,
open,
closeModalAndRefresh
} = this.props;
const {
addLoading,
addError,
page,
rowsPerPage,
permissions,
selectedPermissions,
name,
loadingServiceAccount
} = this.state;
const handleSelectAllClick = (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (event.target.checked) {
// const newSelecteds = permissions.map(n => n.name);
const newSelecteds = [...permissions];
this.setState({ selectedPermissions: newSelecteds });
return;
}
this.setState({ selectedPermissions: [] });
};
const handleClick = (
event: React.MouseEvent<unknown>,
perm: Permission
) => {
let newSelected: Permission[] = [...selectedPermissions];
if (newSelected.filter(p => p.id === perm.id).length === 0) {
newSelected.push(perm);
} else {
let selectedIndex = -1;
for (let i = 0; i < newSelected.length; i++) {
if (newSelected[i].id === perm.id) {
selectedIndex = i;
break;
}
}
if (selectedIndex >= 0) {
newSelected = [
...newSelected.slice(0, selectedIndex),
...newSelected.slice(selectedIndex + 1)
];
}
}
this.setState({ selectedPermissions: newSelected });
};
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage });
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({ page: 0, rowsPerPage: parseInt(event.target.value, 10) });
};
const isSelected = (perm: Permission) =>
selectedPermissions.filter(p => p.id === perm.id).length > 0;
const emptyRows =
rowsPerPage -
Math.min(rowsPerPage, permissions.length - page * rowsPerPage);
const handleChange = (name: string) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({ enabled: event.target.checked });
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
this.setState({ addError: "" }, () => {
closeModalAndRefresh(null);
});
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh(null);
}}
title={`Create Service Account`}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
addServiceAccount(e);
}}
title={
selectedServiceAccount !== null
? "Edit Service Account"
: "Create Service Account"
}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
this.saveRecord(e);
}}
>
<Grid container>
{loadingServiceAccount && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
@@ -383,155 +121,46 @@ class AddServiceAccountContent extends React.Component<
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="service-account-name"
name="service-account-name"
label="Name"
value={name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ name: e.target.value });
<Typography component="h5">
Optional Policy
<Tooltip
title="A policy that restricts this service account can be attached."
placement="top-start"
>
<HelpIcon />
</Tooltip>
</Typography>
<CodeMirror
className={classes.codeMirror}
options={{
mode: "javascript",
lineNumbers: true,
}}
onChange={(editor, data, value) => {
setPolicyDefinition(value);
}}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.root}>
<EnhancedTableToolbar
numSelected={selectedPermissions.length}
/>
<TableContainer>
<Table
className={classes.table}
aria-labelledby="tableTitle"
size={"small"}
aria-label="enhanced table"
>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selectedPermissions.length > 0 &&
selectedPermissions.length < permissions.length
}
checked={
selectedPermissions.length > 0 &&
selectedPermissions.length === permissions.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts"
}}
/>
</TableCell>
<TableCell>Permission</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{permissions
.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
)
.map((row, index) => {
const isItemSelected = isSelected(row);
const labelId = `enhanced-table-checkbox-${index}`;
return (
<TableRow
hover
onClick={event => handleClick(event, row)}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
key={row.name}
selected={isItemSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isItemSelected}
inputProps={{ "aria-labelledby": labelId }}
/>
</TableCell>
<TableCell id={labelId}>{row.name}</TableCell>
<TableCell>{row.description}</TableCell>
</TableRow>
);
})}
{emptyRows > 0 && (
<TableRow style={{ height: 33 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={permissions.length}
rowsPerPage={rowsPerPage}
page={page}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
/>
</div>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch onChange={handleChange("enabled")} value="checkedA" />
}
label="Enabled"
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
}
}
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Create
</Button>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
const AddServiceAccountWrapper = withStyles(styles)(AddServiceAccountContent);
interface IAddServiceAccountProps {
open: boolean;
closeModalAndRefresh: (res: NewServiceAccount | null) => void;
selectedServiceAccount: ServiceAccount | null;
}
interface IAddServiceAccountState {}
class AddServiceAccount extends React.Component<
IAddServiceAccountProps,
IAddServiceAccountState
> {
state: IAddServiceAccountState = {};
render() {
return <AddServiceAccountWrapper {...this.props} />;
}
}
export default AddServiceAccount;
export default withStyles(styles)(AddServiceAccount);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@@ -17,20 +17,19 @@
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { NewServiceAccount } from "./types";
import {
Button,
DialogActions,
DialogContent,
DialogContentText
} from "@material-ui/core";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import Grid from "@material-ui/core/Grid";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
buttonContainer: {
textAlign: "right",
},
});
interface ICredentialsPromptProps {
@@ -40,76 +39,69 @@ interface ICredentialsPromptProps {
closeModal: () => void;
}
interface ICredentialsPromptState {}
const download = (filename: string, text: string) => {
let element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
);
element.setAttribute("download", filename);
class CredentialsPrompt extends React.Component<
ICredentialsPromptProps,
ICredentialsPromptState
> {
state: ICredentialsPromptState = {};
element.style.display = "none";
document.body.appendChild(element);
download(filename: string, text: string) {
var element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
);
element.setAttribute("download", filename);
element.click();
element.style.display = "none";
document.body.appendChild(element);
document.body.removeChild(element);
};
element.click();
document.body.removeChild(element);
const CredentialsPrompt = ({
classes,
newServiceAccount,
open,
closeModal,
}: ICredentialsPromptProps) => {
if (!newServiceAccount) {
return null;
}
render() {
const { classes, open, newServiceAccount } = this.props;
if (newServiceAccount === null) {
return <div />;
}
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
this.props.closeModal();
}}
title="New Service Account"
>
<React.Fragment>
<DialogContent>
<DialogContentText id="alert-dialog-description">
A new service account has been created with the following details:
<ul>
<li>
<b>Access Key:</b>{" "}
{newServiceAccount.service_account.access_key}
</li>
<li>
<b>Secret Key:</b> {newServiceAccount.secret_key}
</li>
</ul>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Write these down, as this is the only time the secret will be
displayed.
</Typography>
</DialogContentText>
</DialogContent>
<DialogActions>
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModal();
}}
title="New Service Account Created"
>
<React.Fragment>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
A new service account has been created with the following details:
<ul>
<li>
<b>Access Key:</b> {newServiceAccount.accessKey}
</li>
<li>
<b>Secret Key:</b> {newServiceAccount.secretKey}
</li>
</ul>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Write these down, as this is the only time the secret will be
displayed.
</Typography>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
onClick={() => {
this.download(
download(
"credentials.json",
JSON.stringify({
access_key: newServiceAccount.service_account.access_key,
secret_key: newServiceAccount.secret_key
access_key: newServiceAccount.accessKey,
secret_key: newServiceAccount.secretKey,
})
);
}}
@@ -119,18 +111,18 @@ class CredentialsPrompt extends React.Component<
</Button>
<Button
onClick={() => {
this.props.closeModal();
closeModal();
}}
color="secondary"
autoFocus
>
Done
</Button>
</DialogActions>
</React.Fragment>
</ModalWrapper>
);
}
}
</Grid>
</Grid>
</React.Fragment>
</ModalWrapper>
);
};
export default withStyles(styles)(CredentialsPrompt);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@@ -14,152 +14,117 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import React from "react";
import Typography from "@material-ui/core/Typography";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@material-ui/core";
import api from "../../../common/api";
import { ServiceAccount, ServiceAccountsList } from "./types";
import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
},
wrapText: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
}
});
createStyles({
errorBlock: {
color: "red",
},
wrapText: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word",
},
});
interface IDeleteServiceAccountProps {
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedServiceAccount: ServiceAccount | null;
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedServiceAccount: string | null;
}
interface IDeleteServiceAccountState {
deleteLoading: boolean;
deleteError: string;
}
const DeleteServiceAccount = ({
classes,
closeDeleteModalAndRefresh,
deleteOpen,
selectedServiceAccount,
}: IDeleteServiceAccountProps) => {
const [deleteLoading, setDeleteLoading] = useState(false);
const [deleteError, setDeleteError] = useState("");
class DeleteServiceAccount extends React.Component<
IDeleteServiceAccountProps,
IDeleteServiceAccountState
> {
state: IDeleteServiceAccountState = {
deleteLoading: false,
deleteError: ""
};
removeRecord() {
const { deleteLoading } = this.state;
const { selectedServiceAccount } = this.props;
if (deleteLoading) {
return;
}
if (selectedServiceAccount == null) {
return;
}
this.setState({ deleteLoading: true }, () => {
api
.invoke("DELETE", `/api/v1/service_accounts/${selectedServiceAccount.id}`, {
id: selectedServiceAccount.id
})
.then((res: ServiceAccountsList) => {
this.setState(
{
deleteLoading: false,
deleteError: ""
},
() => {
this.props.closeDeleteModalAndRefresh(true);
}
);
})
.catch(err => {
this.setState({
deleteLoading: false,
deleteError: err
});
});
useEffect(() => {
if (deleteLoading) {
api
.invoke("DELETE", `/api/v1/service-accounts/${selectedServiceAccount}`)
.then(() => {
setDeleteLoading(false);
setDeleteError("");
closeDeleteModalAndRefresh(true);
})
.catch((err) => {
setDeleteLoading(false);
setDeleteError(err);
});
}
}, [deleteLoading, closeDeleteModalAndRefresh, selectedServiceAccount]);
render() {
const { classes, deleteOpen, selectedServiceAccount } = this.props;
const { deleteLoading, deleteError } = this.state;
if (selectedServiceAccount === null) {
return <div />;
}
return (
<Dialog
open={deleteOpen}
onClose={() => {
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete ServiceAccount</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete service account{" "}
<b className={classes.wrapText}>{selectedServiceAccount.name}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
this.removeRecord();
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
);
const removeRecord = () => {
if (selectedServiceAccount == null) {
return;
}
}
setDeleteLoading(true);
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete ServiceAccount</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete service account{" "}
<b className={classes.wrapText}>{selectedServiceAccount}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button onClick={removeRecord} color="secondary" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
);
};
export default withStyles(styles)(DeleteServiceAccount);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@@ -14,401 +14,295 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import api from "../../../common/api";
import {
Button,
IconButton,
LinearProgress,
TableFooter,
TablePagination
} from "@material-ui/core";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import {
NewServiceAccount,
ServiceAccount,
ServiceAccountsList
} from "./types";
import { NewServiceAccount } from "./types";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import EditIcon from "@material-ui/icons/Edit";
import AddServiceAccount from "./AddServiceAccount";
import DeleteServiceAccount from "./DeleteServiceAccount";
import CredentialsPrompt from "./CredentialsPrompt";
import { CreateIcon, DeleteIcon } from "../../../icons";
import Checkbox from "@material-ui/core/Checkbox";
import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded";
import { CreateIcon } from "../../../icons";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import { stringSort } from "../../../utils/sortFunctions";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
addSideBar: {
width: "480px",
minWidth: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
imageIcon: {
height: "100%"
height: "100%",
},
iconRoot: {
textAlign: "center"
textAlign: "center",
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
}
boxShadow: "0px 3px 6px #00000012",
},
});
interface IServiceAccountsProps {
classes: any;
}
interface IServiceAccountsState {
records: ServiceAccount[];
totalRecords: number;
loading: boolean;
error: string;
deleteError: string;
addScreenOpen: boolean;
page: number;
rowsPerPage: number;
deleteOpen: boolean;
selectedServiceAccount: ServiceAccount | null;
showNewCredentials: boolean;
newServiceAccount: NewServiceAccount | null;
}
const ServiceAccounts = ({ classes }: IServiceAccountsProps) => {
const [records, setRecords] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [filter, setFilter] = useState<string>("");
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedServiceAccount, setSelectedServiceAccount] = useState<
string | null
>(null);
const [showNewCredentials, setShowNewCredentials] = useState<boolean>(false);
const [
newServiceAccount,
setNewServiceAccount,
] = useState<NewServiceAccount | null>(null);
class ServiceAccounts extends React.Component<
IServiceAccountsProps,
IServiceAccountsState
> {
state: IServiceAccountsState = {
records: [],
totalRecords: 0,
loading: false,
error: "",
deleteError: "",
addScreenOpen: false,
page: 0,
rowsPerPage: 10,
deleteOpen: false,
selectedServiceAccount: null,
showNewCredentials: false,
newServiceAccount: null
};
useEffect(() => {
fetchRecords();
}, []);
fetchRecords() {
this.setState({ loading: true }, () => {
const { page, rowsPerPage } = this.state;
const offset = page * rowsPerPage;
useEffect(() => {
if (loading) {
api
.invoke(
"GET",
`/api/v1/service_accounts?offset=${offset}&limit=${rowsPerPage}`
)
.then((res: ServiceAccountsList) => {
this.setState({
loading: false,
records: res.service_accounts,
totalRecords: res.total,
error: ""
});
.invoke("GET", `/api/v1/service-accounts`)
.then((res: string[]) => {
const serviceAccounts = res.sort(stringSort);
setLoading(false);
setRecords(serviceAccounts);
setError("");
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.service_accounts === undefined ||
res.service_accounts == null ||
res.service_accounts.length === 0) &&
page > 0
) {
if ((!serviceAccounts || serviceAccounts.length === 0) && page > 0) {
const newPage = page - 1;
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
setPage(newPage);
fetchRecords();
}
})
.catch(err => {
this.setState({ loading: false, error: err });
.catch((err) => {
setError(err);
setLoading(false);
});
});
}
closeAddModalAndRefresh(res: NewServiceAccount | null) {
this.setState({ addScreenOpen: false }, () => {
this.fetchRecords();
});
if (res !== null) {
this.setState({ showNewCredentials: true, newServiceAccount: res });
}
}
}, [loading, setLoading, setRecords, setError, page, setPage]);
closeDeleteModalAndRefresh(refresh: boolean) {
this.setState({ deleteOpen: false }, () => {
if (refresh) {
this.fetchRecords();
}
});
}
const fetchRecords = () => {
setLoading(true);
};
componentDidMount(): void {
this.fetchRecords();
}
const closeAddModalAndRefresh = (res: NewServiceAccount | null) => {
setAddScreenOpen(false);
fetchRecords();
closeCredentialsModal() {
this.setState({ showNewCredentials: false, newServiceAccount: null });
}
if (res !== null) {
setShowNewCredentials(true);
setNewServiceAccount(res);
}
};
render() {
const { classes } = this.props;
const {
records,
totalRecords,
addScreenOpen,
loading,
page,
rowsPerPage,
deleteOpen,
selectedServiceAccount,
showNewCredentials,
newServiceAccount
} = this.state;
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
};
if (refresh) {
fetchRecords();
}
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
this.setState({ page: 0, rowsPerPage: rPP }, () => {
this.fetchRecords();
});
};
const closeCredentialsModal = () => {
setShowNewCredentials(false);
setNewServiceAccount(null);
};
const confirmDeleteServiceAccount = (
selectedServiceAccount: ServiceAccount
) => {
this.setState({
deleteOpen: true,
selectedServiceAccount: selectedServiceAccount
});
};
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const editServiceAccount = (selectedServiceAccount: ServiceAccount) => {
this.setState({
addScreenOpen: true,
selectedServiceAccount: selectedServiceAccount
});
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
return (
<React.Fragment>
{addScreenOpen && (
<AddServiceAccount
open={addScreenOpen}
selectedServiceAccount={selectedServiceAccount}
closeModalAndRefresh={(res: NewServiceAccount | null) => {
this.closeAddModalAndRefresh(res);
}}
/>
)}
{deleteOpen && (
<DeleteServiceAccount
deleteOpen={deleteOpen}
selectedServiceAccount={selectedServiceAccount}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
)}
{showNewCredentials && (
<CredentialsPrompt
newServiceAccount={newServiceAccount}
open={showNewCredentials}
closeModal={() => {
this.closeCredentialsModal();
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Service Accounts</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Service Accounts"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
this.setState({
addScreenOpen: true,
selectedServiceAccount: null
});
}}
>
Create service account
</Button>
<Button
variant="contained"
color="primary"
startIcon={<PlayArrowRoundedIcon />}
onClick={() => {
this.setState({
addScreenOpen: true
});
}}
>
Change Access
</Button>
</Grid>
setPage(0);
setRowsPerPage(rPP);
};
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
{records != null && records.length > 0 ? (
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Name</TableCell>
<TableCell>Access Key</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map(row => (
<TableRow key={row.name}>
<TableCell padding="checkbox">
<Checkbox
value="secondary"
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.name}
</TableCell>
<TableCell>{row.access_key}</TableCell>
<TableCell>
{row.enabled ? "enabled" : "disabled"}
</TableCell>
<TableCell align="right">
<IconButton
aria-label="edit"
onClick={() => {
editServiceAccount(row);
}}
>
<EditIcon />
</IconButton>
<IconButton
aria-label="delete"
onClick={() => {
confirmDeleteServiceAccount(row);
}}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
colSpan={4}
count={totalRecords}
rowsPerPage={rowsPerPage}
page={page}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true
}}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
ActionsComponent={MinTablePaginationActions}
/>
</TableRow>
</TableFooter>
</Table>
) : (
<div>No Service Accounts</div>
)}
</Paper>
</Grid>
const confirmDeleteServiceAccount = (selectedServiceAccount: string) => {
setSelectedServiceAccount(selectedServiceAccount);
setDeleteOpen(true);
};
const tableActions = [
{ type: "delete", onClick: confirmDeleteServiceAccount },
];
const filteredRecords = records.filter((elementItem) =>
elementItem.toLowerCase().includes(filter.toLowerCase())
);
const beginRecord = page * rowsPerPage;
const endRecords = beginRecord + rowsPerPage;
const paginatedRecords = filteredRecords.slice(beginRecord, endRecords);
return (
<React.Fragment>
{addScreenOpen && (
<AddServiceAccount
open={addScreenOpen}
closeModalAndRefresh={(res: NewServiceAccount | null) => {
closeAddModalAndRefresh(res);
}}
/>
)}
{deleteOpen && (
<DeleteServiceAccount
deleteOpen={deleteOpen}
selectedServiceAccount={selectedServiceAccount}
closeDeleteModalAndRefresh={(refresh: boolean) => {
closeDeleteModalAndRefresh(refresh);
}}
/>
)}
{showNewCredentials && (
<CredentialsPrompt
newServiceAccount={newServiceAccount}
open={showNewCredentials}
closeModal={() => {
closeCredentialsModal();
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Service Accounts</Typography>
</Grid>
</React.Fragment>
);
}
}
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Service Accounts"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
setPage(0);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
setSelectedServiceAccount(null);
}}
>
Create service account
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{error !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{error}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<TableWrapper
isLoading={loading}
records={paginatedRecords}
entityName={"Service Accounts"}
idField={""}
columns={[{ label: "Service Account", elementKey: "" }]}
itemActions={tableActions}
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 4,
count: records.length,
rowsPerPage: rowsPerPage,
page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ServiceAccounts);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@@ -14,27 +14,12 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import {Permission} from "../Permissions/types";
export interface ServiceAccount {
id: string;
name: string;
slug: string;
access_key: string;
enabled: boolean;
}
export interface ServiceAccountsList {
service_accounts: ServiceAccount[];
total:number;
service_accounts: string[];
total: number;
}
export interface NewServiceAccount {
service_account: ServiceAccount,
secret_key:string,
}
export interface ServiceAccountDetails {
service_account: ServiceAccount,
permissions: Permission[],
accessKey: string;
secretKey: string;
}

View File

@@ -0,0 +1,370 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { Button, LinearProgress } from "@material-ui/core";
import api from "../../../../common/api";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import { IVolumeConfiguration, IZone } from "./types";
import CheckboxWrapper from "../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { k8sfactorForDropdown } from "../../../../common/utils";
import ZonesMultiSelector from "./ZonesMultiSelector";
interface IAddTenantProps {
open: boolean;
closeModalAndRefresh: (reloadData: boolean) => any;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
...modalBasic,
});
const AddTenant = ({
open,
closeModalAndRefresh,
classes,
}: IAddTenantProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [addError, setAddError] = useState<string>("");
const [tenantName, setTenantName] = useState<string>("");
const [imageName, setImageName] = useState<string>("");
const [serviceName, setServiceName] = useState<string>("");
const [zones, setZones] = useState<IZone[]>([]);
const [volumesPerServer, setVolumesPerServer] = useState<number>(0);
const [volumeConfiguration, setVolumeConfiguration] = useState<
IVolumeConfiguration
>({ size: "", storage_class: "" });
const [mountPath, setMountPath] = useState<string>("");
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [enableMCS, setEnableMCS] = useState<boolean>(false);
const [enableSSL, setEnableSSL] = useState<boolean>(false);
const [sizeFactor, setSizeFactor] = useState<string>("Gi");
const [storageClasses, setStorageClassesList] = useState<string[]>([]);
useEffect(() => {
fetchStorageClassList();
}, []);
useEffect(() => {
if (addSending) {
let cleanZones: IZone[] = [];
for (let zone of zones) {
if (zone.name !== "") {
cleanZones.push(zone);
}
}
api
.invoke("POST", `/api/v1/mkube/tenants`, {
name: tenantName,
service_name: tenantName,
image: imageName,
enable_ssl: enableSSL,
enable_mcs: enableMCS,
access_key: accessKey,
secret_key: secretKey,
volumes_per_server: volumesPerServer,
volume_configuration: {
size: `${volumeConfiguration.size}${sizeFactor}`,
storage_class: volumeConfiguration.storage_class,
},
zones: cleanZones,
})
.then(() => {
setAddSending(false);
setAddError("");
closeModalAndRefresh(true);
})
.catch((err) => {
setAddSending(false);
setAddError(err);
});
}
}, [addSending]);
const setVolumeConfig = (item: string, value: string) => {
const volumeCopy: IVolumeConfiguration = {
size: item !== "size" ? volumeConfiguration.size : value,
storage_class:
item !== "storage_class" ? volumeConfiguration.storage_class : value,
};
setVolumeConfiguration(volumeCopy);
};
const fetchStorageClassList = () => {
api
.invoke("GET", `/api/v1/mkube/storage-classes`)
.then((res: string[]) => {
let classes: string[] = [];
if (res !== null) {
classes = res;
}
setStorageClassesList(classes);
})
.catch((err: any) => {
console.log(err);
});
};
const storageClassesList = storageClasses.map((s: string) => ({
label: s,
value: s,
}));
return (
<ModalWrapper
title="Create Tenant"
modalOpen={open}
onClose={() => {
setAddError("");
closeModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddSending(true);
}}
>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="tenant-name"
name="tenant-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTenantName(e.target.value);
}}
label="Tenant Name"
value={tenantName}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="image"
name="image"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setImageName(e.target.value);
}}
label="MinIO Image"
value={imageName}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="service_name"
name="service_name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setServiceName(e.target.value);
}}
label="Service Name"
value={serviceName}
/>
</Grid>
<Grid item xs={12}>
<div>
<ZonesMultiSelector
label="Zones"
name="zones_selector"
onChange={(elements: IZone[]) => {
setZones(elements);
}}
elements={zones}
/>
</div>
</Grid>
<Grid item xs={12}>
<Typography component="h3">Volume Configuration</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="volumes_per_server"
name="volumes_per_server"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumesPerServer(parseInt(e.target.value));
}}
label="Volumes per Server"
value={volumesPerServer.toString(10)}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.multiContainer}>
<div>
<InputBoxWrapper
id="volume_size"
name="volume_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumeConfig("size", e.target.value);
}}
label="Size"
value={volumeConfiguration.size}
/>
</div>
<div className={classes.sizeFactorContainer}>
<SelectWrapper
label=""
id="size_factor"
name="size_factor"
value={sizeFactor}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setSizeFactor(e.target.value as string);
}}
options={k8sfactorForDropdown()}
/>
</div>
</div>
</Grid>
<Grid item xs={12}>
<SelectWrapper
id="storage_class"
name="storage_class"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setVolumeConfig("storage_class", e.target.value as string);
}}
label="Storage Class"
value={volumeConfiguration.storage_class}
options={storageClassesList}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="mount_path"
name="mount_path"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setMountPath(e.target.value);
}}
label="Mount Path"
value={mountPath}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="access_key"
name="access_key"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
}}
label="Access Key"
value={accessKey}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="secret_key"
name="secret_key"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
}}
label="Secret Key"
value={secretKey}
/>
</Grid>
<Grid item xs={12}>
<CheckboxWrapper
value="enabled_mcs"
id="enabled_mcs"
name="enabled_mcs"
checked={enableMCS}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
setEnableMCS(checked);
}}
label={"Enable mcs"}
/>
</Grid>
<Grid item xs={12}>
<CheckboxWrapper
value="enable_ssl"
id="enable_ssl"
name="enable_ssl"
checked={enableSSL}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
setEnableSSL(checked);
}}
label={"Enable SSL"}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Save
</Button>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(AddTenant);

View File

@@ -0,0 +1,122 @@
// 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, { useState, useEffect } from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
interface IDeleteTenant {
classes: any;
deleteOpen: boolean;
selectedTenant: string;
closeDeleteModalAndRefresh: (refreshList: boolean) => any;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
});
const DeleteTenant = ({
classes,
deleteOpen,
selectedTenant,
closeDeleteModalAndRefresh,
}: IDeleteTenant) => {
const [deleteLoading, setDeleteLoading] = useState(false);
const [deleteError, setDeleteError] = useState("");
useEffect(() => {
if (deleteLoading) {
api
.invoke("DELETE", `/api/v1/mkube/tenants/${selectedTenant}`)
.then(() => {
setDeleteLoading(false);
setDeleteError("");
closeDeleteModalAndRefresh(true);
})
.catch((err) => {
setDeleteLoading(false);
setDeleteError(err);
});
}
}, [deleteLoading]);
const removeRecord = () => {
setDeleteLoading(true);
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
setDeleteError("");
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Tenant</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete tenant <b>{selectedTenant}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setDeleteError("");
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button onClick={removeRecord} color="secondary" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
);
};
export default withStyles(styles)(DeleteTenant);

View File

@@ -0,0 +1,282 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { Button } from "@material-ui/core";
import { CreateIcon } from "../../../../icons";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
import { ITenant, ITenantsResponse } from "./types";
import { niceBytes } from "../../../../common/utils";
import DeleteTenant from "./DeleteTenant";
import AddTenant from "./AddTenant";
interface ITenantsList {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3),
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px",
},
errorBlock: {
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0),
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
});
const ListTenants = ({ classes }: ITenantsList) => {
const [createTenantOpen, setCreateTenantOpen] = useState<boolean>(false);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedTenant, setSelectedTenant] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [filterTenants, setFilterTenants] = useState<string>("");
const [records, setRecords] = useState<any[]>([]);
const [offset, setOffset] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [page, setPage] = useState<number>(0);
const [error, setError] = useState<string>("");
const closeAddModalAndRefresh = (reloadData: boolean) => {
setCreateTenantOpen(false);
if (reloadData) {
setIsLoading(true);
}
};
const closeDeleteModalAndRefresh = (reloadData: boolean) => {
setDeleteOpen(false);
if (reloadData) {
setIsLoading(true);
}
};
const confirmDeleteTenant = (tenant: string) => {
setSelectedTenant(tenant);
setDeleteOpen(true);
};
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
setPage(0);
setRowsPerPage(rPP);
};
const tableActions = [
{ type: "view", to: `/tenants`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteTenant, sendOnlyId: true },
];
const filteredRecords = records
.slice(offset, offset + rowsPerPage)
.filter((b: any) => {
if (filterTenants === "") {
return true;
} else {
if (b.name.indexOf(filterTenants) >= 0) {
return true;
} else {
return false;
}
}
});
useEffect(() => {
if (isLoading) {
const fetchRecords = () => {
const offset = page * rowsPerPage;
api
.invoke(
"GET",
`/api/v1/mkube/tenants?offset=${offset}&limit=${rowsPerPage}`
)
.then((res: ITenantsResponse) => {
if (res === null) {
setIsLoading(false);
return;
}
let resTenants: ITenant[] = [];
if (res.tenants !== null) {
resTenants = res.tenants;
}
for (let i = 0; i < resTenants.length; i++) {
const total =
resTenants[i].volume_count * resTenants[i].volume_size;
resTenants[i].capacity = niceBytes(total + "");
}
setRecords(resTenants);
setError("");
setIsLoading(false);
// if we get 0 results, and page > 0 , go down 1 page
if ((!res.tenants || res.tenants.length === 0) && page > 0) {
const newPage = page - 1;
setPage(newPage);
}
})
.catch((err) => {
setError(err);
setIsLoading(false);
});
};
fetchRecords();
}
}, [isLoading, page, rowsPerPage]);
useEffect(() => {
setIsLoading(true);
}, []);
return (
<React.Fragment>
{createTenantOpen && (
<AddTenant
open={createTenantOpen}
closeModalAndRefresh={closeAddModalAndRefresh}
/>
)}
{deleteOpen && (
<DeleteTenant
deleteOpen={deleteOpen}
selectedTenant={selectedTenant}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Tenants</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Tenants"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterTenants(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setCreateTenantOpen(true);
}}
>
Create Tenant
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Zones", elementKey: "zone_count" },
{ label: "State", elementKey: "currentState" },
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Tenants"
idField="name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: filteredRecords.length,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ListTenants);

View File

@@ -0,0 +1,183 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 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, { useState, useEffect, createRef, ChangeEvent } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import get from "lodash/get";
import { InputLabel, Tooltip } from "@material-ui/core";
import HelpIcon from "@material-ui/icons/Help";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import {
fieldBasic,
tooltipHelper,
} from "../../Common/FormComponents/common/styleLibrary";
import { IZone } from "./types";
interface IZonesMultiSelector {
elements: IZone[];
name: string;
label: string;
tooltip?: string;
classes: any;
onChange: (elements: IZone[]) => void;
}
const gridBasic = {
display: "grid",
gridTemplateColumns: "calc(50% - 4px) calc(50% - 4px)",
gridGap: 8,
};
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
width: 116,
},
inputContainer: {
height: 150,
overflowY: "auto",
padding: 15,
position: "relative",
border: "1px solid #c4c4c4",
},
labelContainer: {
display: "flex",
},
inputGrid: {
...gridBasic,
},
inputTitles: {
...gridBasic,
marginBottom: 5,
},
});
const ZonesMultiSelector = ({
elements,
name,
label,
tooltip = "",
onChange,
classes,
}: IZonesMultiSelector) => {
const defaultZone: IZone = { name: "", servers: 0 };
const [currentElements, setCurrentElements] = useState<IZone[]>([
{ ...defaultZone },
]);
const bottomList = createRef<HTMLDivElement>();
// Use effect to send new values to onChange
useEffect(() => {
onChange(currentElements);
}, [currentElements]);
// If the last input is not empty, we add a new one
const addEmptyRow = (elementsUp: IZone[]) => {
const lastElement = elementsUp[elementsUp.length - 1];
if (lastElement.servers !== 0 && lastElement.name !== "") {
elementsUp.push({ ...defaultZone });
const refScroll = bottomList.current;
if (refScroll) {
refScroll.scrollIntoView(false);
}
}
return elementsUp;
};
// Onchange function for input box, we get the dataset-index & only update that value in the array
const onChangeElement = (e: ChangeEvent<HTMLInputElement>, field: string) => {
e.persist();
let updatedElement = [...currentElements];
const index = get(e.target, "dataset.index", 0);
const rowPosition: IZone = updatedElement[index];
rowPosition.servers =
field === "servers" ? parseInt(e.target.value) : rowPosition.servers;
rowPosition.name = field === "name" ? e.target.value : rowPosition.name;
updatedElement[index] = rowPosition;
updatedElement = addEmptyRow(updatedElement);
setCurrentElements(updatedElement);
};
const inputs = currentElements.map((element, index) => {
return (
<React.Fragment key={`zone-${name}-${index.toString()}`}>
<div>
<InputBoxWrapper
id={`${name}-${index.toString()}-name`}
label={""}
name={`${name}-${index.toString()}-name`}
value={currentElements[index].name}
onChange={(e) => onChangeElement(e, "name")}
index={index}
key={`Zones-${name}-${index.toString()}-name`}
/>
</div>
<div>
<InputBoxWrapper
type="number"
id={`${name}-${index.toString()}-servers`}
label={""}
name={`${name}-${index.toString()}-servers`}
value={currentElements[index].servers.toString(10)}
onChange={(e) => onChangeElement(e, "servers")}
index={index}
key={`Zones-${name}-${index.toString()}-servers`}
/>
</div>
</React.Fragment>
);
});
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
<Grid item xs={12}>
<div className={classes.inputTitles}>
<div>Name</div>
<div>Servers</div>
</div>
<div className={classes.inputContainer}>
<div className={classes.inputGrid}>{inputs}</div>
</div>
<div ref={bottomList} />
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ZonesMultiSelector);

View File

@@ -0,0 +1,41 @@
// 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 IZone {
name: string;
servers: number;
}
export interface IVolumeConfiguration {
size: string;
storage_class: string;
}
export interface ITenant {
name: string;
zone_count: number;
currentState: string;
instance_count: 4;
creation_date: Date;
volume_size: number;
volume_count: number;
// computed
capacity: string;
}
export interface ITenantsResponse {
tenants: ITenant[];
}

View File

@@ -0,0 +1,201 @@
import React, { useState } from "react";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import Grid from "@material-ui/core/Grid";
import { factorForDropdown, getTotalSize } from "../../../../common/utils";
import { Button, LinearProgress } from "@material-ui/core";
interface IAddZoneProps {
classes: any;
open: boolean;
onCloseZoneAndReload: (shouldReload: boolean) => void;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
bottomContainer: {
display: "flex",
flexGrow: 1,
alignItems: "center",
"& div": {
flexGrow: 1,
width: "100%",
},
},
factorElements: {
display: "flex",
justifyContent: "flex-start",
},
sizeNumber: {
fontSize: 35,
fontWeight: 700,
textAlign: "center",
},
sizeDescription: {
fontSize: 14,
color: "#777",
textAlign: "center",
},
...modalBasic,
});
const AddZoneModal = ({
classes,
open,
onCloseZoneAndReload,
}: IAddZoneProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [zoneName, setZoneName] = useState<string>("");
const [numberOfInstances, setNumberOfInstances] = useState<number>(0);
const [volumesPerInstance, setVolumesPerInstance] = useState<number>(0);
const [sizeFactor, setSizeFactor] = useState<string>("GiB");
const [volumeConfiguration, setVolumeConfig] = useState<string>("");
const [storageClass, setStorageClass] = useState<string>("");
const instanceCapacity: number =
parseFloat(volumeConfiguration) * volumesPerInstance;
const totalCapacity: number = instanceCapacity * numberOfInstances;
return (
<ModalWrapper
onClose={() => onCloseZoneAndReload(false)}
modalOpen={open}
title="Add Zone"
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddSending(true);
}}
>
<Grid item xs={12}>
<InputBoxWrapper
id="zone_name"
name="zone_name"
type="string"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setZoneName(e.target.value);
}}
label="Name"
value={zoneName}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="number_instances"
name="number_instances"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setNumberOfInstances(parseInt(e.target.value));
}}
label="Volumes per Server"
value={numberOfInstances.toString(10)}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="volumes_per_instance"
name="volumes_per_instance"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumesPerInstance(parseInt(e.target.value));
}}
label="Volumes per Instance"
value={volumesPerInstance.toString(10)}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.multiContainer}>
<div>
<InputBoxWrapper
id="volume_size"
name="volume_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumeConfig(e.target.value);
}}
label="Size"
value={volumeConfiguration}
/>
</div>
<div className={classes.sizeFactorContainer}>
<SelectWrapper
label=""
id="size_factor"
name="size_factor"
value={sizeFactor}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setSizeFactor(e.target.value as string);
}}
options={factorForDropdown()}
/>
</div>
</div>
<Grid item xs={12}>
<InputBoxWrapper
id="storage_class"
name="storage_class"
type="string"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setStorageClass(e.target.value);
}}
label="Volumes per Server"
value={storageClass}
/>
</Grid>
<Grid item xs={12} className={classes.bottomContainer}>
<div className={classes.factorElements}>
<div>
<div className={classes.sizeNumber}>
{getTotalSize(instanceCapacity.toString(10), sizeFactor)}
</div>
<div className={classes.sizeDescription}>Instance Capacity</div>
</div>
<div>
<div className={classes.sizeNumber}>
{getTotalSize(totalCapacity.toString(10), sizeFactor)}
</div>
<div className={classes.sizeDescription}>Total Capacity</div>
</div>
</div>
<div className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Save
</Button>
</div>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(AddZoneModal);

View File

@@ -0,0 +1,218 @@
// 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, { useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { Button, LinearProgress } from "@material-ui/core";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import Tab from "@material-ui/core/Tab";
import Tabs from "@material-ui/core/Tabs";
interface IReplicationProps {
classes: any;
open: boolean;
closeModalAndRefresh: (refreshList: boolean) => void;
}
interface IDropDownElements {
label: string;
value: string;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
...modalBasic,
});
const ReplicationSetup = ({
classes,
open,
closeModalAndRefresh,
}: IReplicationProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [selectedTab, setSelectedTab] = useState<number>(0);
const [sourceBucket, setSourceBucket] = useState<string>("");
const [clusterSelected, setClusterSelected] = useState<string>("");
const [destinationBucket, setDestinationBucket] = useState<string>("");
const [address, setAddress] = useState<string>("");
const [bucket, setBucket] = useState<string>("");
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const clustersList: IDropDownElements[] = [];
const sourceBuckets: IDropDownElements[] = [];
const destinationBuckets: IDropDownElements[] = [];
return (
<ModalWrapper
modalOpen={open}
title="Add Zone"
onClose={() => {
closeModalAndRefresh(false);
}}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddSending(true);
}}
>
<Grid item xs={12}>
<SelectWrapper
label="Source Bucket"
options={sourceBuckets}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setSourceBucket(e.target.value as string);
}}
value={sourceBucket}
name="source_bucket"
id="source_bucket"
/>
</Grid>
<Grid item xs={12}>
<Tabs
value={selectedTab}
indicatorColor="primary"
textColor="primary"
onChange={(_, newValue: number) => {
setSelectedTab(newValue);
}}
aria-label="cluster-tabs"
>
<Tab label="Local Cluster" />
<Tab label="Remote Cluster" />
</Tabs>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{selectedTab === 0 && (
<React.Fragment>
<Grid item xs={12}>
<SelectWrapper
label="Cluster"
options={clustersList}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setClusterSelected(e.target.value as string);
}}
value={clusterSelected}
name="cluster"
id="cluster"
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
label="Destination Bucket"
options={destinationBuckets}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setDestinationBucket(e.target.value as string);
}}
value={destinationBucket}
name="destination_bucket"
id="destination_bucket"
/>
</Grid>
</React.Fragment>
)}
{selectedTab === 1 && (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="address"
name="address"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAddress(e.target.value);
}}
label="Address"
value={address}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="bucket"
name="bucket"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setBucket(e.target.value);
}}
label="Bucket"
value={bucket}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="accessKey"
name="accessKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
}}
label="Access Key"
value={accessKey}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="secretKey"
name="secretKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
}}
label="Secret Key"
value={secretKey}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Save
</Button>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(ReplicationSetup);

View File

@@ -0,0 +1,428 @@
// 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, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button } from "@material-ui/core";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import { CreateIcon } from "../../../../icons";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import Paper from "@material-ui/core/Paper";
import { niceBytes } from "../../../../common/utils";
import AddZoneModal from "./AddZoneModal";
import AddBucket from "../../Buckets/ListBuckets/AddBucket";
import ReplicationSetup from "./ReplicationSetup";
interface ITenantDetailsProps {
classes: any;
match: any;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
containerHeader: {
display: "flex",
justifyContent: "space-between",
},
paperContainer: {
padding: "15px 15px 15px 50px",
},
infoGrid: {
display: "grid",
gridTemplateColumns: "auto auto auto auto",
gridGap: 8,
"& div": {
display: "flex",
alignItems: "center",
},
"& div:nth-child(odd)": {
justifyContent: "flex-end",
fontWeight: 700,
},
"& div:nth-child(2n)": {
paddingRight: 35,
},
},
masterActions: {
width: "25%",
minWidth: "120px",
"& div": {
margin: "5px 0px",
},
},
actionsTray: {
textAlign: "right",
},
...modalBasic,
});
const mainPagination = {
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: 0,
rowsPerPage: 0,
page: 0,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
ActionsComponent: MinTablePaginationActions,
};
const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
const [selectedTab, setSelectedTab] = useState<number>(0);
const [capacity, setCapacity] = useState<number>(0);
const [externalIDP, setExternalIDP] = useState<boolean>(false);
const [externalKMS, setExternalKMS] = useState<boolean>(false);
const [zones, setZones] = useState<number>(0);
const [instances, setInstances] = useState<number>(0);
const [drives, setDrives] = useState<number>(0);
const [addZoneOpen, setAddZone] = useState<boolean>(false);
const [addBucketOpen, setAddBucketOpen] = useState<boolean>(false);
const [addReplicationOpen, setAddReplicationOpen] = useState<boolean>(false);
const onCloseZoneAndRefresh = (reload: boolean) => {
setAddZone(false);
if (reload) {
console.log("reload");
}
};
const closeBucketsAndRefresh = () => {
setAddBucketOpen(false);
};
const closeReplicationAndRefresh = (reload: boolean) => {
setAddReplicationOpen(false);
if (reload) {
console.log("reload");
}
};
return (
<React.Fragment>
{addZoneOpen && (
<AddZoneModal
open={addZoneOpen}
onCloseZoneAndReload={onCloseZoneAndRefresh}
/>
)}
{addBucketOpen && (
<AddBucket
open={addBucketOpen}
closeModalAndRefresh={closeBucketsAndRefresh}
/>
)}
{addReplicationOpen && (
<ReplicationSetup
open={addReplicationOpen}
closeModalAndRefresh={closeReplicationAndRefresh}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">
Tenant > {match.params["clusterName"]}
</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.containerHeader}>
<Paper className={classes.paperContainer}>
<div className={classes.infoGrid}>
<div>Capacity:</div>
<div>{niceBytes(capacity.toString(10))}</div>
<div>Zones:</div>
<div>{zones}</div>
<div>External IDP:</div>
<div>
{externalIDP ? "Yes" : "No"}&nbsp;&nbsp;
<Button
variant="contained"
color="primary"
size="small"
onClick={() => {}}
>
Edit
</Button>
</div>
<div>Instances:</div>
<div>{instances}</div>
<div>External KMS:</div>
<div>
{externalKMS ? "Yes" : "No"}&nbsp;&nbsp;
<Button
variant="contained"
color="primary"
size="small"
onClick={() => {}}
>
Edit
</Button>
</div>
<div>Drives:</div>
<div>{drives}</div>
</div>
</Paper>
<div className={classes.masterActions}>
<div>
<Button
variant="contained"
color="primary"
fullWidth
onClick={() => {}}
>
Warp
</Button>
</div>
<div>
<Button
variant="contained"
color="primary"
fullWidth
onClick={() => {}}
>
See as deployment
</Button>
</div>
</div>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={6}>
<Tabs
value={selectedTab}
indicatorColor="primary"
textColor="primary"
onChange={(_, newValue: number) => {
setSelectedTab(newValue);
}}
aria-label="tenant-tabs"
>
<Tab label="Zones" />
<Tab label="Buckets" />
<Tab label="Replication" />
</Tabs>
</Grid>
<Grid item xs={6} className={classes.actionsTray}>
{selectedTab === 0 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddZone(true);
}}
>
Add Zone
</Button>
)}
{selectedTab === 1 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddBucketOpen(true);
}}
>
Create Bucket
</Button>
)}
{selectedTab === 2 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddReplicationOpen(true);
}}
>
Add Replication
</Button>
)}
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
{selectedTab === 0 && (
<TableWrapper
itemActions={[
{
type: "view",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "delete",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
]}
columns={[
{
label: "Status",
elementKey: "status",
},
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Instances", elementKey: "instances" },
{ label: "# of Drives", elementKey: "drives" },
]}
isLoading={false}
records={[]}
entityName="Zones"
idField="name"
paginatorConfig={{
...mainPagination,
onChangePage: () => {},
onChangeRowsPerPage: () => {},
}}
/>
)}
{selectedTab === 1 && (
<TableWrapper
itemActions={[
{
type: "view",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "replicate",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "mirror",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "delete",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
]}
columns={[
{
label: "Status",
elementKey: "status",
},
{ label: "Name", elementKey: "name" },
{ label: "AccessPolicy", elementKey: "access_policy" },
]}
isLoading={false}
records={[]}
entityName="Buckets"
idField="name"
paginatorConfig={{
...mainPagination,
onChangePage: () => {},
onChangeRowsPerPage: () => {},
}}
/>
)}
{selectedTab === 2 && (
<TableWrapper
itemActions={[
{
type: "view",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
]}
columns={[
{
label: "Source",
elementKey: "source",
},
{ label: "Source Bucket", elementKey: "source_bucket" },
{ label: "Destination", elementKey: "destination" },
{
label: "Destination Bucket",
elementKey: "destination_bucket",
},
]}
isLoading={false}
records={[]}
entityName="Replication"
idField="id"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: 0,
rowsPerPage: 0,
page: 0,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: () => {},
onChangeRowsPerPage: () => {},
ActionsComponent: MinTablePaginationActions,
}}
/>
)}
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(TenantDetails);

View File

@@ -31,15 +31,15 @@ const styles = (theme: Theme) =>
overflow: "auto",
"& ul": {
margin: "4px",
padding: "0px"
padding: "0px",
},
"& ul li": {
listStyle: "none",
margin: "0px",
padding: "0px",
borderBottom: "1px solid #dedede"
}
}
borderBottom: "1px solid #dedede",
},
},
});
interface ITrace {
@@ -53,7 +53,7 @@ const Trace = ({
classes,
traceMessageReceived,
traceResetMessages,
messages
messages,
}: ITrace) => {
useEffect(() => {
traceResetMessages();
@@ -96,7 +96,7 @@ const Trace = ({
<h1>Trace</h1>
<div className={classes.logList}>
<ul>
{messages.map(m => {
{messages.map((m) => {
return (
<li key={m.key}>
{timeFromDate(m.time)} - {m.api}[{m.statusCode} {m.statusMsg}]{" "}
@@ -113,12 +113,12 @@ const Trace = ({
};
const mapState = (state: AppState) => ({
messages: state.trace.messages
messages: state.trace.messages,
});
const connector = connect(mapState, {
traceMessageReceived: traceMessageReceived,
traceResetMessages: traceResetMessages
traceResetMessages: traceResetMessages,
});
export default connector(withStyles(styles)(Trace));

View File

@@ -35,12 +35,12 @@ export type TraceActionTypes =
export function traceMessageReceived(message: TraceMessage) {
return {
type: TRACE_MESSAGE_RECEIVED,
message: message
message: message,
};
}
export function traceResetMessages() {
return {
type: TRACE_RESET_MESSAGES
type: TRACE_RESET_MESSAGES,
};
}

View File

@@ -17,7 +17,7 @@
import {
TRACE_MESSAGE_RECEIVED,
TRACE_RESET_MESSAGES,
TraceActionTypes
TraceActionTypes,
} from "./actions";
import { TraceMessage } from "./types";
@@ -26,7 +26,7 @@ export interface TraceState {
}
const initialState: TraceState = {
messages: []
messages: [],
};
export function traceReducer(
@@ -37,12 +37,12 @@ export function traceReducer(
case TRACE_MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message]
messages: [...state.messages, action.message],
};
case TRACE_RESET_MESSAGES:
return {
...state,
messages: []
messages: [],
};
default:
return state;

View File

@@ -24,7 +24,7 @@ import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import TextField from "@material-ui/core/TextField";
import api from "../../../common/api";
import { groupsSort } from "../../../utils/sortFunctions";
import { stringSort } from "../../../utils/sortFunctions";
import { GroupsList } from "../Groups/types";
import get from "lodash/get";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
@@ -38,43 +38,43 @@ interface IGroupsProps {
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "left",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
filterField: {
background: "#FFFFFF",
@@ -82,24 +82,24 @@ const styles = (theme: Theme) =>
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: "100%",
zIndex: 500
zIndex: 500,
},
noFound: {
textAlign: "center",
padding: "10px 0"
padding: "10px 0",
},
tableContainer: {
maxHeight: 200
maxHeight: 200,
},
stickyHeader: {
backgroundColor: "#fff"
}
backgroundColor: "#fff",
},
});
const GroupsSelectors = ({
classes,
selectedGroups,
setSelectedGroups
setSelectedGroups,
}: IGroupsProps) => {
// Local State
const [records, setRecords] = useState<any[]>([]);
@@ -129,11 +129,11 @@ const GroupsSelectors = ({
if (!groups) {
groups = [];
}
setRecords(groups.sort(groupsSort));
setRecords(groups.sort(stringSort));
setError("");
isLoading(false);
})
.catch(err => {
.catch((err) => {
setError(err);
isLoading(false);
});
@@ -151,14 +151,14 @@ const GroupsSelectors = ({
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
elements = elements.filter((element) => element !== value);
}
setSelectedGroups(elements);
return elements;
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.includes(filter)
);
@@ -183,9 +183,9 @@ const GroupsSelectors = ({
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
onChange={(e) => {
setFilter(e.target.value);
}}
/>

View File

@@ -22,7 +22,7 @@ import {
Grid,
Typography,
TextField,
InputAdornment
InputAdornment,
} from "@material-ui/core";
import SearchIcon from "@material-ui/icons/Search";
import GroupIcon from "@material-ui/icons/Group";
@@ -34,54 +34,56 @@ import AddUser from "./AddUser";
import DeleteUser from "./DeleteUser";
import AddToGroup from "./AddToGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import DescriptionIcon from "@material-ui/icons/Description";
import SetPolicy from "../Policies/SetPolicy";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
}
boxShadow: "0px 3px 6px #00000012",
},
});
interface IUsersProps {
@@ -102,6 +104,7 @@ interface IUsersState {
addGroupOpen: boolean;
filter: string;
checkedUsers: string[];
setPolicyOpen: boolean;
}
class Users extends React.Component<IUsersProps, IUsersState> {
@@ -118,7 +121,8 @@ class Users extends React.Component<IUsersProps, IUsersState> {
selectedUser: null,
addGroupOpen: false,
filter: "",
checkedUsers: []
checkedUsers: [],
setPolicyOpen: false,
};
fetchRecords() {
@@ -133,7 +137,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
loading: false,
records: users.sort(usersSort),
totalRecords: users.length,
error: ""
error: "",
});
// if we get 0 results, and page > 0 , go down 1 page
if ((!users || users.length === 0) && page > 0) {
@@ -143,7 +147,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
});
}
})
.catch(err => {
.catch((err) => {
this.setState({ loading: false, error: err });
});
});
@@ -191,7 +195,8 @@ class Users extends React.Component<IUsersProps, IUsersState> {
selectedUser,
filter,
checkedUsers,
addGroupOpen
addGroupOpen,
setPolicyOpen,
} = this.state;
const handleChangePage = (event: unknown, newPage: number) => {
@@ -205,7 +210,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
this.setState({ page: 0, rowsPerPage: rPP });
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.accessKey.includes(filter)
);
@@ -226,11 +231,11 @@ class Users extends React.Component<IUsersProps, IUsersState> {
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
elements = elements.filter((element) => element !== value);
}
this.setState({
checkedUsers: elements
checkedUsers: elements,
});
return elements;
@@ -239,20 +244,28 @@ class Users extends React.Component<IUsersProps, IUsersState> {
const viewAction = (selectionElement: any): void => {
this.setState({
addScreenOpen: true,
selectedUser: selectionElement
selectedUser: selectionElement,
});
};
const setPolicyAction = (selectionElement: any): void => {
this.setState({
setPolicyOpen: true,
selectedUser: selectionElement,
});
};
const deleteAction = (selectionElement: any): void => {
this.setState({
deleteOpen: true,
selectedUser: selectionElement
selectedUser: selectionElement,
});
};
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "delete", onClick: deleteAction }
{ type: "description", onClick: setPolicyAction },
{ type: "delete", onClick: deleteAction },
];
return (
@@ -266,6 +279,16 @@ class Users extends React.Component<IUsersProps, IUsersState> {
}}
/>
)}
{setPolicyOpen && (
<SetPolicy
open={setPolicyOpen}
selectedUser={selectedUser}
selectedGroup={null}
closeModalAndRefresh={() => {
this.setState({ setPolicyOpen: false });
}}
/>
)}
{deleteOpen && (
<DeleteUser
deleteOpen={deleteOpen}
@@ -304,9 +327,9 @@ class Users extends React.Component<IUsersProps, IUsersState> {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
onChange={(e) => {
this.setState({ filter: e.target.value, page: 0 });
}}
/>
@@ -318,7 +341,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
onClick={() => {
if (checkedUsers.length > 0) {
this.setState({
addGroupOpen: true
addGroupOpen: true,
});
}
}}
@@ -332,7 +355,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
onClick={() => {
this.setState({
addScreenOpen: true,
selectedUser: null
selectedUser: null,
});
}}
>
@@ -361,11 +384,11 @@ class Users extends React.Component<IUsersProps, IUsersState> {
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>

View File

@@ -14,12 +14,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, { useEffect, useState } from "react";
import {
Button,
Grid,
Typography,
TextField
} from "@material-ui/core";
import { Button, Grid, Typography, TextField } from "@material-ui/core";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { AppState } from "../../../store";
import { connect } from "react-redux";
@@ -29,11 +24,7 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { niceBytes, timeFromDate } from "../../../common/utils";
import { wsProtocol } from "../../../utils/wsUtils";
import api from "../../../common/api";
import {
FormControl,
MenuItem,
Select,
} from "@material-ui/core";
import { FormControl, MenuItem, Select } from "@material-ui/core";
const styles = (theme: Theme) =>
createStyles({
@@ -43,27 +34,27 @@ const styles = (theme: Theme) =>
overflow: "auto",
"& ul": {
margin: "4px",
padding: "0px"
padding: "0px",
},
"& ul li": {
listStyle: "none",
margin: "0px",
padding: "0px",
borderBottom: "1px solid #dedede"
}
borderBottom: "1px solid #dedede",
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
}
},
},
inputField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
marginLeft: 10,
boxShadow: "0px 3px 6px #00000012"
boxShadow: "0px 3px 6px #00000012",
},
fieldContainer: {
background: "#FFFFFF",
@@ -72,8 +63,8 @@ const styles = (theme: Theme) =>
marginLeft: 10,
textAlign: "left",
minWidth: "206px",
boxShadow: "0px 3px 6px #00000012"
}
boxShadow: "0px 3px 6px #00000012",
},
});
interface IWatch {
@@ -87,7 +78,7 @@ const Watch = ({
classes,
watchMessageReceived,
watchResetMessages,
messages
messages,
}: IWatch) => {
const [start, setStart] = useState(false);
const [bucketName, setBucketName] = useState("Select Bucket");
@@ -108,7 +99,7 @@ const Watch = ({
.catch((err: any) => {
console.log(err);
});
}
};
useEffect(() => {
fetchBucketList();
}, []);
@@ -116,13 +107,15 @@ const Watch = ({
useEffect(() => {
watchResetMessages();
// begin watch if bucketName in bucketList and start pressed
if (start && bucketList.some(bucket => bucket.name === bucketName)) {
if (start && bucketList.some((bucket) => bucket.name === bucketName)) {
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket(`${wsProt}://${url.hostname}:${port}/ws/watch/${bucketName}?prefix=${prefix}&suffix=${suffix}`);
const c = new W3CWebSocket(
`${wsProt}://${url.hostname}:${port}/ws/watch/${bucketName}?prefix=${prefix}&suffix=${suffix}`
);
let interval: any | null = null;
if (c !== null) {
@@ -156,9 +149,9 @@ const Watch = ({
}
}, [watchMessageReceived, start]);
const bucketNames = bucketList.map(bucketName => ({
const bucketNames = bucketList.map((bucketName) => ({
label: bucketName.name,
value: bucketName.name
value: bucketName.name,
}));
return (
<React.Fragment>
@@ -175,7 +168,9 @@ const Watch = ({
id="bucket-name"
name="bucket-name"
value={bucketName}
onChange={(e) => { setBucketName(e.target.value as string) }}
onChange={(e) => {
setBucketName(e.target.value as string);
}}
className={classes.fieldContainer}
disabled={start}
>
@@ -185,8 +180,8 @@ const Watch = ({
disabled={true}
>
Select Bucket
</MenuItem>
{bucketNames.map(option => (
</MenuItem>
{bucketNames.map((option) => (
<MenuItem
value={option.value}
key={`select-bucket-name-${option.label}`}
@@ -205,7 +200,9 @@ const Watch = ({
InputProps={{
disableUnderline: true,
}}
onChange={(e) => { setPrefix(e.target.value) }}
onChange={(e) => {
setPrefix(e.target.value);
}}
/>
<TextField
placeholder="Suffix"
@@ -216,7 +213,9 @@ const Watch = ({
InputProps={{
disableUnderline: true,
}}
onChange={(e) => { setSuffix(e.target.value) }}
onChange={(e) => {
setSuffix(e.target.value);
}}
/>
<Button
type="submit"
@@ -226,7 +225,7 @@ const Watch = ({
onClick={() => setStart(true)}
>
Start
</Button>
</Button>
</Grid>
<Grid item xs={12}>
<br />
@@ -234,10 +233,11 @@ const Watch = ({
</Grid>
<div className={classes.watchList}>
<ul>
{messages.map(m => {
{messages.map((m) => {
return (
<li key={m.key}>
{timeFromDate(m.Time)} - {niceBytes(m.Size + "")} - {m.Type} - {m.Path}
{timeFromDate(m.Time)} - {niceBytes(m.Size + "")} - {m.Type} -{" "}
{m.Path}
</li>
);
})}
@@ -248,12 +248,12 @@ const Watch = ({
};
const mapState = (state: AppState) => ({
messages: state.watch.messages
messages: state.watch.messages,
});
const connector = connect(mapState, {
watchMessageReceived: watchMessageReceived,
watchResetMessages: watchResetMessages
watchResetMessages: watchResetMessages,
});
export default connector(withStyles(styles)(Watch));

View File

@@ -28,7 +28,6 @@ interface WatchResetMessagesAction {
type: typeof WATCH_RESET_MESSAGES;
}
export type WatchActionTypes =
| WatchMessageReceivedAction
| WatchResetMessagesAction;
@@ -36,12 +35,12 @@ export type WatchActionTypes =
export function watchMessageReceived(message: EventInfo) {
return {
type: WATCH_MESSAGE_RECEIVED,
message: message
message: message,
};
}
export function watchResetMessages() {
return {
type: WATCH_RESET_MESSAGES
type: WATCH_RESET_MESSAGES,
};
}

View File

@@ -17,7 +17,7 @@
import {
WATCH_MESSAGE_RECEIVED,
WATCH_RESET_MESSAGES,
WatchActionTypes
WatchActionTypes,
} from "./actions";
import { EventInfo } from "./types";
@@ -26,7 +26,7 @@ export interface WatchState {
}
const initialState: WatchState = {
messages: []
messages: [],
};
export function watchReducer(
@@ -37,12 +37,12 @@ export function watchReducer(
case WATCH_MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message]
messages: [...state.messages, action.message],
};
case WATCH_RESET_MESSAGES:
return {
...state,
messages: []
messages: [],
};
default:
return state;

View File

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

View File

@@ -0,0 +1,44 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { ISessionResponse } from "./types";
import { SessionActionTypes, SESSION_RESPONSE } from "./actions";
export interface ConsoleState {
session: ISessionResponse;
}
const initialState: ConsoleState = {
session: {
status: "",
pages: [],
},
};
export function consoleReducer(
state = initialState,
action: SessionActionTypes
): ConsoleState {
switch (action.type) {
case SESSION_RESPONSE:
return {
...state,
session: action.message,
};
default:
return state;
}
}

View File

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

View File

@@ -30,7 +30,7 @@ const LoginCallback: FC<RouteComponentProps> = ({ location }) => {
// store the jwt token
storage.setItem("token", res.sessionId);
// We push to history the new URL.
window.location.href = "/dashboard";
window.location.href = "/";
}
})
.catch((res: any) => {

View File

@@ -35,8 +35,8 @@ const styles = (theme: Theme) =>
createStyles({
"@global": {
body: {
backgroundColor: "#F4F4F4"
}
backgroundColor: "#F4F4F4",
},
},
paper: {
marginTop: theme.spacing(16),
@@ -45,48 +45,48 @@ const styles = (theme: Theme) =>
flexDirection: "column",
alignItems: "center",
width: "800px",
margin: "auto"
margin: "auto",
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main
backgroundColor: theme.palette.secondary.main,
},
form: {
width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
submit: {
margin: theme.spacing(3, 0, 2)
margin: theme.spacing(3, 0, 2),
},
errorBlock: {
color: "red"
color: "red",
},
mainContainer: {
borderRadius: "3px"
borderRadius: "3px",
},
theOcean: {
borderTopLeftRadius: "3px",
borderBottomLeftRadius: "3px",
background:
"transparent linear-gradient(333deg, #281B6F 1%, #271260 13%, #120D53 83%) 0% 0% no-repeat padding-box;"
"transparent linear-gradient(333deg, #281B6F 1%, #271260 13%, #120D53 83%) 0% 0% no-repeat padding-box;",
},
oceanBg: {
backgroundImage: "url(/images/BG_Illustration.svg)",
backgroundRepeat: "no-repeat",
backgroundPosition: "bottom left",
height: "100%",
width: "100%"
width: "100%",
},
theLogin: {
padding: "76px 62px 20px 62px"
padding: "76px 62px 20px 62px",
},
loadingLoginStrategy: {
textAlign: "center"
}
textAlign: "center",
},
});
const mapState = (state: SystemState) => ({
loggedIn: state.loggedIn
loggedIn: state.loggedIn,
});
const connector = connect(mapState, { userLoggedIn });
@@ -117,8 +117,8 @@ class Login extends React.Component<ILoginProps, ILoginState> {
loading: false,
loginStrategy: {
loginStrategy: "",
redirect: ""
}
redirect: "",
},
};
fetchConfiguration() {
@@ -127,12 +127,12 @@ class Login extends React.Component<ILoginProps, ILoginState> {
.invoke("GET", "/api/v1/login")
.then((loginDetails: ILoginDetails) => {
this.setState({
loading: false
loading: false,
});
this.setState({
loading: false,
loginStrategy: loginDetails,
error: ""
error: "",
});
})
.catch((err: any) => {
@@ -165,10 +165,11 @@ class Login extends React.Component<ILoginProps, ILoginState> {
.then(() => {
// We set the state in redux
this.props.userLoggedIn(true);
// We push to history the new URL.
history.push("/dashboard");
// There is a browser cache issue if we change the policy associated to an account and then logout and history.push("/") after login
// therefore after login we need to use window.location redirect
window.location.href = "/";
})
.catch(err => {
.catch((err) => {
this.setState({ error: `${err}` });
});
};

View File

@@ -23,22 +23,21 @@ import Container from "@material-ui/core/Container";
import Copyright from "../common/Copyright";
import history from "../history";
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
"@global": {
body: {
backgroundColor: theme.palette.common.white
}
backgroundColor: theme.palette.common.white,
},
},
paper: {
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center"
}
alignItems: "center",
},
}));
const NotFound: React.FC = () => {
const classes = useStyles();
console.log(history);
return (
<Container component="main">
<CssBaseline />

View File

@@ -20,12 +20,14 @@ import { systemReducer } from "./reducer";
import { traceReducer } from "./screens/Console/Trace/reducers";
import { logReducer } from "./screens/Console/Logs/reducers";
import { watchReducer } from "./screens/Console/Watch/reducers";
import { consoleReducer } from "./screens/Console/reducer";
const globalReducer = combineReducers({
system: systemReducer,
trace: traceReducer,
logs: logReducer,
watch: watchReducer,
console: consoleReducer,
});
declare global {

View File

@@ -18,6 +18,10 @@ interface userInterface {
accessKey: string;
}
interface policyInterface {
name: string;
}
export const usersSort = (a: userInterface, b: userInterface) => {
if (a.accessKey > b.accessKey) {
return 1;
@@ -29,7 +33,18 @@ export const usersSort = (a: userInterface, b: userInterface) => {
return 0;
};
export const groupsSort = (a: string, b: string) => {
export const policySort = (a: policyInterface, b: policyInterface) => {
if (a.name > b.name) {
return 1;
}
if (a.name < b.name) {
return -1;
}
// a must be equal to b
return 0;
};
export const stringSort = (a: string, b: string) => {
if (a > b) {
return 1;
}

View File

@@ -3013,6 +3013,29 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
chart.js@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7"
integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==
dependencies:
chartjs-color "^2.1.0"
moment "^2.10.2"
chartjs-color-string@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
dependencies:
color-name "^1.0.0"
chartjs-color@^2.1.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
dependencies:
chartjs-color-string "^0.6.0"
color-convert "^1.9.3"
chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -3243,7 +3266,7 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
color-convert@^1.9.0, color-convert@^1.9.1:
color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -7632,7 +7655,7 @@ lodash.without@~4.4.0:
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.5, lodash@~4.17.4:
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.4:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -8028,6 +8051,11 @@ mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.0, mkdir
dependencies:
minimist "^1.2.5"
moment@^2.10.2:
version "2.26.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
@@ -9959,7 +9987,7 @@ promzard@^0.3.0:
dependencies:
read "1"
prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.8, prop-types@^15.6.0, 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==
@@ -10176,6 +10204,14 @@ react-app-polyfill@^1.0.6:
regenerator-runtime "^0.13.3"
whatwg-fetch "^3.0.0"
react-chartjs-2@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-2.9.0.tgz#d054dbdd763fbe9a76296a4ae0752ea549b76d9e"
integrity sha512-IYwqUUnQRAJ9SNA978vxulHJTcUFTJk2LDVfbAyk0TnJFZZG7+6U/2flsE4MCw6WCbBjTTypy8T82Ch7XrPtRw==
dependencies:
lodash "^4.17.4"
prop-types "^15.5.8"
react-codemirror2@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-7.1.0.tgz#b874a275ad4f6f2ee5adb23b550c0f4b8b82776d"

View File

@@ -28,12 +28,9 @@ import (
"github.com/go-openapi/swag"
"github.com/stretchr/testify/assert"
"github.com/minio/minio/pkg/event/target"
"github.com/minio/minio/cmd/config"
"github.com/minio/mcs/models"
"github.com/minio/minio/cmd/config"
"github.com/minio/minio/pkg/event/target"
"github.com/minio/minio/pkg/madmin"
)
@@ -532,7 +529,7 @@ func Test_getConfig(t *testing.T) {
// mock function response from getConfig()
minioGetConfigKVMock = func(key string) ([]byte, error) {
return nil, errors.New("Invalid config")
return nil, errors.New("invalid config")
}
mockConfigList := madmin.Help{}
@@ -553,7 +550,7 @@ func Test_getConfig(t *testing.T) {
mock: func() {
// mock function response from getConfig()
minioGetConfigKVMock = func(key string) ([]byte, error) {
return nil, errors.New("Invalid config")
return nil, errors.New("invalid config")
}
// mock function response from listConfig()
minioHelpConfigKVMock = func(subSys, key string, envOnly bool) (madmin.Help, error) {

View File

@@ -22,7 +22,6 @@ import (
"fmt"
"log"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
@@ -32,97 +31,46 @@ import (
const logTimeFormat string = "15:04:05 MST 01/02/2006"
// startConsoleLog starts log of the servers
// by first setting a websocket reader that will
// check for a heartbeat.
//
// A WaitGroup is used to handle goroutines and to ensure
// all finish in the proper order. If any, sendConsoleLogInfo()
// or wsReadCheck() returns, trace should end.
func startConsoleLog(conn WSConn, client MinioAdmin) (mError error) {
// a WaitGroup waits for a collection of goroutines to finish
wg := sync.WaitGroup{}
// a cancel context is needed to end all goroutines used
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
func startConsoleLog(ctx context.Context, conn WSConn, client MinioAdmin) error {
// TODO: accept parameters as variables
// name of node, default = "" (all)
node := ""
// number of log lines
lineCount := 100
// type of logs "minio"|"application"|"all" default = "all"
logKind := "all"
// Start listening on all Console Log activity.
logCh := client.getLogs(ctx, node, lineCount, logKind)
// Set number of goroutines to wait. wg.Wait()
// waitsuntil counter is zero (all are done)
wg.Add(3)
// start go routine for reading websocket heartbeat
readErr := wsReadCheck(ctx, &wg, conn)
// send Stream of Console Log Info to the ws c.connection
logCh := sendConsoleLogInfo(ctx, &wg, conn, client)
// If wsReadCheck returns it means that it is not possible to check
// ws heartbeat anymore so we stop from doing Console Log, cancel context
// for all goroutines.
go func(wg *sync.WaitGroup) {
defer wg.Done()
if err := <-readErr; err != nil {
log.Println("error on wsReadCheck:", err)
mError = err
}
// cancel context for all goroutines.
cancel()
}(&wg)
// get logCh err on finish
if err := <-logCh; err != nil {
mError = err
}
// if logCh closes for any reason,
// cancel context for all goroutines
cancel()
// wait all goroutines to finish
wg.Wait()
return mError
}
// sendlogInfo sends stream of Console Log Info to the ws connection
func sendConsoleLogInfo(ctx context.Context, wg *sync.WaitGroup, conn WSConn, client MinioAdmin) <-chan error {
// decrements the WaitGroup counter
// by one when the function returns
defer wg.Done()
ch := make(chan error)
go func(ch chan<- error) {
defer close(ch)
// TODO: accept parameters as variables
// name of node, default = "" (all)
node := ""
// number of log lines
lineCount := 100
// type of logs "minio"|"application"|"all" default = "all"
logKind := "all"
// Start listening on all Console Log activity.
logCh := client.getLogs(ctx, node, lineCount, logKind)
for logInfo := range logCh {
for {
select {
case <-ctx.Done():
return nil
case logInfo, ok := <-logCh:
// zero value returned because the channel is closed and empty
if !ok {
return nil
}
if logInfo.Err != nil {
log.Println("error on console logs:", logInfo.Err)
ch <- logInfo.Err
return
return logInfo.Err
}
// Serialize message to be sent
bytes, err := json.Marshal(serializeConsoleLogInfo(&logInfo))
if err != nil {
fmt.Println("error on json.Marshal:", err)
ch <- err
return
return err
}
// Send Message through websocket connection
err = conn.writeMessage(websocket.TextMessage, bytes)
if err != nil {
log.Println("error writeMessage:", err)
ch <- err
return
return err
}
}
}(ch)
return ch
}
}
func serializeConsoleLogInfo(l *madmin.LogInfo) (logInfo madmin.LogInfo) {

Some files were not shown because too many files have changed in this diff Show More