Compare commits

..

23 Commits

Author SHA1 Message Date
Minio Trusted
63350e5492 update to v0.4.4 2020-11-05 18:07:46 -08:00
Alex
255c128b67 Fixed pagination un buckets lists (#371)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-11-05 10:16:03 -08:00
Cesar N
06f333395e Move trace and logs UI to Operator Console (#375)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-11-04 21:45:48 -08:00
Alex
3cd024ea2c Changed sizes for main container & table paper (#377)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-11-04 16:36:51 -08:00
Alex
9c0a407db6 Fixed page refresh on buckets creation (#372)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-11-04 15:25:36 -08:00
Alex
dc3c619f3f Added animation & disabled button / fields on sending (#369)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-11-03 11:12:41 -08:00
Alex
5000aafba6 Added functionality for create folder & replaced icons (#368)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-11-02 21:45:47 -08:00
Cesar N
b9f2a39d50 Fix tenant creation (#363)
Memory size request was not being sent, hence the tenant was not being created.
2020-11-02 11:03:40 -08:00
Daniel Valdivia
df321191f4 Build assets inside the container (#357) 2020-11-01 07:34:37 -08:00
Alex
547eb41e96 Added navigation to object browser (#358)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-10-31 00:22:46 -07:00
Lenin Alevski
afbb83e081 certs-dir value was ignored because of wrong variable assigning (#362) 2020-10-30 22:36:52 -07:00
Lenin Alevski
b599968570 SNI support for Console (#352)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-10-29 22:26:48 -07:00
Cesar N
24cc60f34e Add put object api and add list object improvements (#356) 2020-10-28 16:08:26 -07:00
Alex
f967058409 Updated Heal section (#351)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-10-27 21:06:24 -07:00
Alex
078e09ba76 Fixed inconsistences in create tenant modal (#349)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-10-27 12:43:12 -07:00
Lenin Alevski
d6f97841d4 return generic login error when invalid credentials (#350) 2020-10-27 10:05:16 -07:00
Lenin Alevski
619ac124b3 Bucket encryption checkbox and endpoints for Console (#343)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-10-25 12:56:23 -07:00
Alex
a2180e123d Removed warnings on console (#345)
For Operator modules this will be removed after upgrading component to use redux/formik

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-10-24 00:05:26 -07:00
Cesar N
0325bb7e2d Add put object retention api (#340) 2020-10-23 15:04:02 -07:00
Minio Trusted
fce361e5bd update to v0.4.3 2020-10-23 02:15:25 -07:00
Alex
ed6d6e8b9d Fixed audit issues (#342)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-10-23 02:03:49 -07:00
Alex
406709f66b Updated Watch view to have console consistent styles (#341)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-10-22 17:15:40 -07:00
Cesar N
3ac45a2211 Add Set object's legalhold status api (#339)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-10-22 16:23:29 -07:00
147 changed files with 9023 additions and 4873 deletions

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
go-version: [1.14.x] go-version: [1.15.x]
os: [ubuntu-latest] os: [ubuntu-latest]
steps: steps:
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }} - name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}

View File

@@ -12,6 +12,10 @@ before:
hooks: hooks:
# you may remove this if you don't use vgo # you may remove this if you don't use vgo
- go mod tidy - go mod tidy
- docker build -f Dockerfile.assets -t consoleassets .
- docker create --name extract consoleassets
- docker cp extract:/app/bindata_assetfs.go ./portal-ui/
- docker rm extract
builds: builds:
- -

View File

@@ -1,4 +1,25 @@
FROM golang:1.13 FROM golang:1.15 as binlayer
RUN go get github.com/go-bindata/go-bindata/... && go get github.com/elazarl/go-bindata-assetfs/...
FROM node:10 as uilayer
WORKDIR /app
COPY --from=binlayer /go/bin/go-bindata-assetfs /bin/
COPY --from=binlayer /go/bin/go-bindata /bin/
COPY ./portal-ui/package.json ./
COPY ./portal-ui/yarn.lock ./
RUN yarn install
COPY ./portal-ui .
RUN yarn install && make build-static
USER node
FROM golang:1.15 as golayer
RUN apt-get update -y && apt-get install -y ca-certificates RUN apt-get update -y && apt-get install -y ca-certificates
@@ -12,6 +33,8 @@ RUN go mod download
ADD . /go/src/github.com/minio/console/ ADD . /go/src/github.com/minio/console/
WORKDIR /go/src/github.com/minio/console/ WORKDIR /go/src/github.com/minio/console/
COPY --from=uilayer /app/bindata_assetfs.go /go/src/github.com/minio/console/portal-ui/
ENV CGO_ENABLED=0 ENV CGO_ENABLED=0
RUN go build -ldflags "-w -s" -a -o console ./cmd/console RUN go build -ldflags "-w -s" -a -o console ./cmd/console
@@ -20,7 +43,7 @@ FROM scratch
MAINTAINER MinIO Development "dev@min.io" MAINTAINER MinIO Development "dev@min.io"
EXPOSE 9090 EXPOSE 9090
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=golayer /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=0 /go/src/github.com/minio/console/console . COPY --from=golayer /go/src/github.com/minio/console/console .
ENTRYPOINT ["/console"] ENTRYPOINT ["/console"]

20
Dockerfile.assets Normal file
View File

@@ -0,0 +1,20 @@
FROM golang:1.15 as binlayer
RUN go get github.com/go-bindata/go-bindata/... && go get github.com/elazarl/go-bindata-assetfs/...
FROM node:10 as uilayer
WORKDIR /app
COPY --from=binlayer /go/bin/go-bindata-assetfs /bin/
COPY --from=binlayer /go/bin/go-bindata /bin/
COPY ./portal-ui/package.json ./
COPY ./portal-ui/yarn.lock ./
RUN yarn install
COPY ./portal-ui .
RUN yarn install && make build-static
USER node

View File

@@ -113,11 +113,41 @@ export CONSOLE_MINIO_SERVER=http://localhost:9000
./console server ./console server
``` ```
## Connect Console to a Minio using TLS and a self-signed certificate ## Run Console with TLS enable
Copy your `public.crt` and `private.key` to `~/.console/certs`, then:
```bash
./console server
```
Additionally, `Console` has support for multiple certificates, clients can request them using `SNI`. It expects the following structure:
```bash
certs/
├─ public.crt
├─ private.key
├─ example.com/
│ │
│ ├─ public.crt
│ └─ private.key
└─ foobar.org/
├─ public.crt
└─ private.key
...
``` ```
...
export CONSOLE_MINIO_SERVER_TLS_ROOT_CAS=<certificate_file_name> Therefore, we read all filenames in the cert directory and check
for each directory whether it contains a public.crt and private.key.
## Connect Console to a Minio using TLS and a self-signed certificate
Copy the MinIO `ca.crt` under `~/.console/certs/CAs`, then:
```
export CONSOLE_MINIO_SERVER=https://localhost:9000 export CONSOLE_MINIO_SERVER=https://localhost:9000
./console server ./console server
``` ```

File diff suppressed because one or more lines are too long

View File

@@ -20,12 +20,16 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"path/filepath"
"github.com/go-openapi/loads" "github.com/go-openapi/loads"
"github.com/jessevdk/go-flags" "github.com/jessevdk/go-flags"
"github.com/minio/cli" "github.com/minio/cli"
"github.com/minio/console/pkg/certs"
"github.com/minio/console/restapi" "github.com/minio/console/restapi"
"github.com/minio/console/restapi/operations" "github.com/minio/console/restapi/operations"
"github.com/minio/minio/cmd/logger"
certsx "github.com/minio/minio/pkg/certs"
) )
// starts the server // starts the server
@@ -56,14 +60,9 @@ var serverCmd = cli.Command{
Usage: "HTTPS server port", Usage: "HTTPS server port",
}, },
cli.StringFlag{ cli.StringFlag{
Name: "tls-certificate", Name: "certs-dir",
Value: "", Value: certs.GlobalCertsCADir.Get(),
Usage: "filename of public cert", Usage: "path to certs directory",
},
cli.StringFlag{
Name: "tls-key",
Value: "",
Usage: "filename of private key",
}, },
}, },
} }
@@ -82,7 +81,9 @@ func startServer(ctx *cli.Context) error {
parser := flags.NewParser(server, flags.Default) parser := flags.NewParser(server, flags.Default)
parser.ShortDescription = "MinIO Console Server" parser.ShortDescription = "MinIO Console Server"
parser.LongDescription = swaggerSpec.Spec().Info.Description parser.LongDescription = swaggerSpec.Spec().Info.Description
server.ConfigureFlags() server.ConfigureFlags()
for _, optsGroup := range api.CommandLineOptionsGroups { for _, optsGroup := range api.CommandLineOptionsGroups {
_, err := parser.AddGroup(optsGroup.ShortDescription, optsGroup.LongDescription, optsGroup.Options) _, err := parser.AddGroup(optsGroup.ShortDescription, optsGroup.LongDescription, optsGroup.Options)
if err != nil { if err != nil {
@@ -106,12 +107,19 @@ func startServer(ctx *cli.Context) error {
restapi.Hostname = ctx.String("host") restapi.Hostname = ctx.String("host")
restapi.Port = fmt.Sprintf("%v", ctx.Int("port")) restapi.Port = fmt.Sprintf("%v", ctx.Int("port"))
tlsCertificatePath := ctx.String("tls-certificate") // Set all certs and CAs directories.
tlsCertificateKeyPath := ctx.String("tls-key") certs.GlobalCertsDir, _ = certs.NewConfigDirFromCtx(ctx, "certs-dir", certs.DefaultCertsDir.Get)
certs.GlobalCertsCADir = &certs.ConfigDir{Path: filepath.Join(certs.GlobalCertsDir.Get(), certs.CertsCADir)}
logger.FatalIf(certs.MkdirAllIgnorePerm(certs.GlobalCertsCADir.Get()), "Unable to create certs CA directory at %s", certs.GlobalCertsCADir.Get())
if tlsCertificatePath != "" && tlsCertificateKeyPath != "" { // load all CAs from ~/.console/certs/CAs
server.TLSCertificate = flags.Filename(tlsCertificatePath) restapi.GlobalRootCAs, err = certsx.GetRootCAs(certs.GlobalCertsCADir.Get())
server.TLSCertificateKey = flags.Filename(tlsCertificateKeyPath) logger.FatalIf(err, "Failed to read root CAs (%v)", err)
// load all certs from ~/.console/certs
restapi.GlobalPublicCerts, restapi.GlobalTLSCertsManager, err = certs.GetTLSConfig()
logger.FatalIf(err, "Unable to load the TLS configuration")
if len(restapi.GlobalPublicCerts) > 0 && restapi.GlobalRootCAs != nil {
// If TLS certificates are provided enforce the HTTPS schema, meaning console will redirect // If TLS certificates are provided enforce the HTTPS schema, meaning console will redirect
// plain HTTP connections to HTTPS server // plain HTTP connections to HTTPS server
server.EnabledListeners = []string{"http", "https"} server.EnabledListeners = []string{"http", "https"}

5
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/minio/console module github.com/minio/console
go 1.13 go 1.15
require ( require (
github.com/coreos/go-oidc v2.2.1+incompatible github.com/coreos/go-oidc v2.2.1+incompatible
@@ -19,7 +19,8 @@ require (
github.com/minio/mc v0.0.0-20201001165056-7f2df96e4821 github.com/minio/mc v0.0.0-20201001165056-7f2df96e4821
github.com/minio/minio v0.0.0-20200927172404-27d9bd04e544 github.com/minio/minio v0.0.0-20200927172404-27d9bd04e544
github.com/minio/minio-go/v7 v7.0.6-0.20200923173112-bc846cb9b089 github.com/minio/minio-go/v7 v7.0.6-0.20200923173112-bc846cb9b089
github.com/minio/operator v0.0.0-20200930213302-ab2bbdfae96c github.com/minio/operator v0.0.0-20201022162018-527e5c32132b
github.com/mitchellh/go-homedir v1.1.0
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/secure-io/sio-go v0.3.1 github.com/secure-io/sio-go v0.3.1
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1

11
go.sum
View File

@@ -60,10 +60,12 @@ github.com/Azure/azure-storage-blob-go v0.10.0/go.mod h1:ep1edmW+kNQx4UfWM9heESN
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest v14.1.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.1.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0= github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0=
github.com/Azure/go-autorest/autorest v0.10.2 h1:NuSF3gXetiHyUbVdneJMEVyPUYAe5wh+aN08JYAf1tI=
github.com/Azure/go-autorest/autorest v0.10.2/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= github.com/Azure/go-autorest/autorest v0.10.2/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU= github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
@@ -72,11 +74,13 @@ github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMl
github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/adal v0.8.3/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= github.com/Azure/go-autorest/autorest/adal v0.8.3/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
github.com/Azure/go-autorest/autorest/adal v0.9.1 h1:xjPqigMQe2+0DAJ5A6MLUPp5D2r2Io8qHCuCMMI/yJU=
github.com/Azure/go-autorest/autorest/adal v0.9.1/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/adal v0.9.1/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM= github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM=
github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw= github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
@@ -86,8 +90,10 @@ github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocm
github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA=
github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8=
github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI= github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI=
github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -540,6 +546,7 @@ github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTV
github.com/googleapis/gnostic v0.2.2 h1:DcFegQ7+ECdmkJMfVwWlC+89I4esJ7p8nkGt9ainGDk= github.com/googleapis/gnostic v0.2.2 h1:DcFegQ7+ECdmkJMfVwWlC+89I4esJ7p8nkGt9ainGDk=
github.com/googleapis/gnostic v0.2.2/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.2.2/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gookit/color v1.2.4/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gookit/color v1.2.4/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -798,8 +805,8 @@ github.com/minio/minio-go/v7 v7.0.2/go.mod h1:dJ80Mv2HeGkYLH1sqS/ksz07ON6csH3S6J
github.com/minio/minio-go/v7 v7.0.5-0.20200811211821-14ed05478889/go.mod h1:CSt2ETZNs+bIIhWTse0mcZKZWMGrFU7Er7RR0TmkDYk= github.com/minio/minio-go/v7 v7.0.5-0.20200811211821-14ed05478889/go.mod h1:CSt2ETZNs+bIIhWTse0mcZKZWMGrFU7Er7RR0TmkDYk=
github.com/minio/minio-go/v7 v7.0.6-0.20200923173112-bc846cb9b089 h1:9DDs/Gc3fNHOQxQmwIFWs7YDLMTBh59r2XQ6RqEUA1I= github.com/minio/minio-go/v7 v7.0.6-0.20200923173112-bc846cb9b089 h1:9DDs/Gc3fNHOQxQmwIFWs7YDLMTBh59r2XQ6RqEUA1I=
github.com/minio/minio-go/v7 v7.0.6-0.20200923173112-bc846cb9b089/go.mod h1:CSt2ETZNs+bIIhWTse0mcZKZWMGrFU7Er7RR0TmkDYk= github.com/minio/minio-go/v7 v7.0.6-0.20200923173112-bc846cb9b089/go.mod h1:CSt2ETZNs+bIIhWTse0mcZKZWMGrFU7Er7RR0TmkDYk=
github.com/minio/operator v0.0.0-20200930213302-ab2bbdfae96c h1:OIKdzEJDFmUokbJ1rIdlr3kcfsBfXelYgSCTN/+Ppec= github.com/minio/operator v0.0.0-20201022162018-527e5c32132b h1:ggfD6V3nodTzhHJHCYIT1F897gscrz+hHsan0a2Wtqw=
github.com/minio/operator v0.0.0-20200930213302-ab2bbdfae96c/go.mod h1:6lavbNo2YuJWeQR5bZYsEWdbpRCO2KrTyfQ0PtC/AN4= github.com/minio/operator v0.0.0-20201022162018-527e5c32132b/go.mod h1:At+++4/6W5BEXK11tN5DKIvkJKhYBZybbb5zmxb0LQI=
github.com/minio/selfupdate v0.3.1 h1:BWEFSNnrZVMUWXbXIgLDNDjbejkmpAmZvy/nCz1HlEs= github.com/minio/selfupdate v0.3.1 h1:BWEFSNnrZVMUWXbXIgLDNDjbejkmpAmZvy/nCz1HlEs=
github.com/minio/selfupdate v0.3.1/go.mod h1:b8ThJzzH7u2MkF6PcIra7KaXO9Khf6alWPvMSyTDCFM= github.com/minio/selfupdate v0.3.1/go.mod h1:b8ThJzzH7u2MkF6PcIra7KaXO9Khf6alWPvMSyTDCFM=
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=

View File

@@ -15,7 +15,7 @@ spec:
serviceAccountName: console-sa serviceAccountName: console-sa
containers: containers:
- name: console - name: console
image: minio/console:v0.4.2 image: minio/console:v0.4.4
imagePullPolicy: "IfNotPresent" imagePullPolicy: "IfNotPresent"
args: args:
- server - server

View File

@@ -15,7 +15,7 @@ spec:
serviceAccountName: console-sa serviceAccountName: console-sa
containers: containers:
- name: console - name: console
image: minio/console:v0.4.2 image: minio/console:v0.4.4
imagePullPolicy: "IfNotPresent" imagePullPolicy: "IfNotPresent"
env: env:
- name: CONSOLE_OPERATOR_MODE - name: CONSOLE_OPERATOR_MODE

View File

@@ -0,0 +1,63 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// BucketEncryptionInfo bucket encryption info
//
// swagger:model bucketEncryptionInfo
type BucketEncryptionInfo struct {
// algorithm
Algorithm string `json:"algorithm,omitempty"`
// kms master key ID
KmsMasterKeyID string `json:"kmsMasterKeyID,omitempty"`
}
// Validate validates this bucket encryption info
func (m *BucketEncryptionInfo) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *BucketEncryptionInfo) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *BucketEncryptionInfo) UnmarshalBinary(b []byte) error {
var res BucketEncryptionInfo
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,89 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// BucketEncryptionRequest bucket encryption request
//
// swagger:model bucketEncryptionRequest
type BucketEncryptionRequest struct {
// enc type
EncType BucketEncryptionType `json:"encType,omitempty"`
// kms key ID
KmsKeyID string `json:"kmsKeyID,omitempty"`
}
// Validate validates this bucket encryption request
func (m *BucketEncryptionRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateEncType(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *BucketEncryptionRequest) validateEncType(formats strfmt.Registry) error {
if swag.IsZero(m.EncType) { // not required
return nil
}
if err := m.EncType.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("encType")
}
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *BucketEncryptionRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *BucketEncryptionRequest) UnmarshalBinary(b []byte) error {
var res BucketEncryptionRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,80 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
)
// BucketEncryptionType bucket encryption type
//
// swagger:model bucketEncryptionType
type BucketEncryptionType string
const (
// BucketEncryptionTypeSseS3 captures enum value "sse-s3"
BucketEncryptionTypeSseS3 BucketEncryptionType = "sse-s3"
// BucketEncryptionTypeSseKms captures enum value "sse-kms"
BucketEncryptionTypeSseKms BucketEncryptionType = "sse-kms"
)
// for schema
var bucketEncryptionTypeEnum []interface{}
func init() {
var res []BucketEncryptionType
if err := json.Unmarshal([]byte(`["sse-s3","sse-kms"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
bucketEncryptionTypeEnum = append(bucketEncryptionTypeEnum, v)
}
}
func (m BucketEncryptionType) validateBucketEncryptionTypeEnum(path, location string, value BucketEncryptionType) error {
if err := validate.EnumCase(path, location, value, bucketEncryptionTypeEnum, true); err != nil {
return err
}
return nil
}
// Validate validates this bucket encryption type
func (m BucketEncryptionType) Validate(formats strfmt.Registry) error {
var res []error
// value enum
if err := m.validateBucketEncryptionTypeEnum("", "body", m); err != nil {
return err
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -65,6 +65,9 @@ type BucketObject struct {
// size // size
Size int64 `json:"size,omitempty"` Size int64 `json:"size,omitempty"`
// tags
Tags map[string]string `json:"tags,omitempty"`
// user tags // user tags
UserTags map[string]string `json:"user_tags,omitempty"` UserTags map[string]string `json:"user_tags,omitempty"`

View File

@@ -93,9 +93,6 @@ type CreateTenantRequest struct {
// secret key // secret key
SecretKey string `json:"secret_key,omitempty"` SecretKey string `json:"secret_key,omitempty"`
// service name
ServiceName string `json:"service_name,omitempty"`
// tls // tls
TLS *TLSConfiguration `json:"tls,omitempty"` TLS *TLSConfiguration `json:"tls,omitempty"`

View File

@@ -0,0 +1,80 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
)
// ObjectLegalHoldStatus object legal hold status
//
// swagger:model objectLegalHoldStatus
type ObjectLegalHoldStatus string
const (
// ObjectLegalHoldStatusEnabled captures enum value "enabled"
ObjectLegalHoldStatusEnabled ObjectLegalHoldStatus = "enabled"
// ObjectLegalHoldStatusDisabled captures enum value "disabled"
ObjectLegalHoldStatusDisabled ObjectLegalHoldStatus = "disabled"
)
// for schema
var objectLegalHoldStatusEnum []interface{}
func init() {
var res []ObjectLegalHoldStatus
if err := json.Unmarshal([]byte(`["enabled","disabled"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
objectLegalHoldStatusEnum = append(objectLegalHoldStatusEnum, v)
}
}
func (m ObjectLegalHoldStatus) validateObjectLegalHoldStatusEnum(path, location string, value ObjectLegalHoldStatus) error {
if err := validate.EnumCase(path, location, value, objectLegalHoldStatusEnum, true); err != nil {
return err
}
return nil
}
// Validate validates this object legal hold status
func (m ObjectLegalHoldStatus) Validate(formats strfmt.Registry) error {
var res []error
// value enum
if err := m.validateObjectLegalHoldStatusEnum("", "body", m); err != nil {
return err
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -0,0 +1,80 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
)
// ObjectRetentionMode object retention mode
//
// swagger:model objectRetentionMode
type ObjectRetentionMode string
const (
// ObjectRetentionModeGovernance captures enum value "governance"
ObjectRetentionModeGovernance ObjectRetentionMode = "governance"
// ObjectRetentionModeCompliance captures enum value "compliance"
ObjectRetentionModeCompliance ObjectRetentionMode = "compliance"
)
// for schema
var objectRetentionModeEnum []interface{}
func init() {
var res []ObjectRetentionMode
if err := json.Unmarshal([]byte(`["governance","compliance"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
objectRetentionModeEnum = append(objectRetentionModeEnum, v)
}
}
func (m ObjectRetentionMode) validateObjectRetentionModeEnum(path, location string, value ObjectRetentionMode) error {
if err := validate.EnumCase(path, location, value, objectRetentionModeEnum, true); err != nil {
return err
}
return nil
}
// Validate validates this object retention mode
func (m ObjectRetentionMode) Validate(formats strfmt.Registry) error {
var res []error
// value enum
if err := m.validateObjectRetentionModeEnum("", "body", m); err != nil {
return err
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -0,0 +1,83 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// PutObjectLegalHoldRequest put object legal hold request
//
// swagger:model putObjectLegalHoldRequest
type PutObjectLegalHoldRequest struct {
// status
// Required: true
Status ObjectLegalHoldStatus `json:"status"`
}
// Validate validates this put object legal hold request
func (m *PutObjectLegalHoldRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateStatus(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *PutObjectLegalHoldRequest) validateStatus(formats strfmt.Registry) error {
if err := m.Status.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("status")
}
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *PutObjectLegalHoldRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *PutObjectLegalHoldRequest) UnmarshalBinary(b []byte) error {
var res PutObjectLegalHoldRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,104 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// PutObjectRetentionRequest put object retention request
//
// swagger:model putObjectRetentionRequest
type PutObjectRetentionRequest struct {
// expires
// Required: true
Expires *string `json:"expires"`
// governance bypass
GovernanceBypass bool `json:"governance_bypass,omitempty"`
// mode
// Required: true
Mode ObjectRetentionMode `json:"mode"`
}
// Validate validates this put object retention request
func (m *PutObjectRetentionRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateExpires(formats); err != nil {
res = append(res, err)
}
if err := m.validateMode(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *PutObjectRetentionRequest) validateExpires(formats strfmt.Registry) error {
if err := validate.Required("expires", "body", m.Expires); err != nil {
return err
}
return nil
}
func (m *PutObjectRetentionRequest) validateMode(formats strfmt.Registry) error {
if err := m.Mode.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("mode")
}
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *PutObjectRetentionRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *PutObjectRetentionRequest) UnmarshalBinary(b []byte) error {
var res PutObjectRetentionRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,60 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// PutObjectTagsRequest put object tags request
//
// swagger:model putObjectTagsRequest
type PutObjectTagsRequest struct {
// tags
Tags map[string]string `json:"tags,omitempty"`
}
// Validate validates this put object tags request
func (m *PutObjectTagsRequest) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *PutObjectTagsRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *PutObjectTagsRequest) UnmarshalBinary(b []byte) error {
var res PutObjectTagsRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -22,27 +22,26 @@ import (
// endpoints definition // endpoints definition
var ( var (
configuration = "/configurations-list" configuration = "/configurations-list"
users = "/users" users = "/users"
groups = "/groups" groups = "/groups"
iamPolicies = "/policies" iamPolicies = "/policies"
dashboard = "/dashboard" dashboard = "/dashboard"
profiling = "/profiling" profiling = "/profiling"
trace = "/trace" watch = "/watch"
logs = "/logs" notifications = "/notification-endpoints"
watch = "/watch" buckets = "/buckets"
notifications = "/notification-endpoints" bucketsDetail = "/buckets/:bucketName"
buckets = "/buckets" serviceAccounts = "/service-accounts"
bucketsDetail = "/buckets/:bucketName" tenants = "/tenants"
serviceAccounts = "/service-accounts" tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
tenants = "/tenants" heal = "/heal"
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName" remoteBuckets = "/remote-buckets"
heal = "/heal" replication = "/replication"
remoteBuckets = "/remote-buckets" objectBrowser = "/object-browser/:bucket/*"
replication = "/replication" objectBrowserBucket = "/object-browser/:bucket"
objectBrowser = "/object-browser/:bucket?" mainObjectBrowser = "/object-browser"
mainObjectBrowser = "/object-browser" license = "/license"
license = "/license"
) )
type ConfigurationActionSet struct { type ConfigurationActionSet struct {
@@ -60,16 +59,6 @@ var configurationActionSet = ConfigurationActionSet{
), ),
} }
// 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 // dashboardActionSet contains the list of admin actions required for this endpoint to work
var dashboardActionSet = ConfigurationActionSet{ var dashboardActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet( actionTypes: iampolicy.NewActionSet(
@@ -118,16 +107,6 @@ var profilingActionSet = ConfigurationActionSet{
), ),
} }
// 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 // usersActionSet contains the list of admin actions required for this endpoint to work
var usersActionSet = ConfigurationActionSet{ var usersActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet( actionTypes: iampolicy.NewActionSet(
@@ -245,25 +224,24 @@ var licenseActionSet = ConfigurationActionSet{
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here // endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
var endpointRules = map[string]ConfigurationActionSet{ var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet, configuration: configurationActionSet,
users: usersActionSet, users: usersActionSet,
groups: groupsActionSet, groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet, iamPolicies: iamPoliciesActionSet,
dashboard: dashboardActionSet, dashboard: dashboardActionSet,
profiling: profilingActionSet, profiling: profilingActionSet,
trace: traceActionSet, watch: watchActionSet,
logs: logsActionSet, notifications: notificationsActionSet,
watch: watchActionSet, buckets: bucketsActionSet,
notifications: notificationsActionSet, bucketsDetail: bucketsActionSet,
buckets: bucketsActionSet, serviceAccounts: serviceAccountsActionSet,
bucketsDetail: bucketsActionSet, heal: healActionSet,
serviceAccounts: serviceAccountsActionSet, remoteBuckets: remoteBucketsActionSet,
heal: healActionSet, replication: replicationActionSet,
remoteBuckets: remoteBucketsActionSet, objectBrowser: objectBrowserActionSet,
replication: replicationActionSet, mainObjectBrowser: objectBrowserActionSet,
objectBrowser: objectBrowserActionSet, objectBrowserBucket: objectBrowserActionSet,
mainObjectBrowser: objectBrowserActionSet, license: licenseActionSet,
license: licenseActionSet,
} }
// operatorRules contains the mapping between endpoints and ActionSets for operator only mode // operatorRules contains the mapping between endpoints and ActionSets for operator only mode

View File

@@ -50,7 +50,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
args: args{ args: args{
[]string{"admin:ServerInfo"}, []string{"admin:ServerInfo"},
}, },
want: 5, want: 6,
}, },
{ {
name: "policies endpoint", name: "policies endpoint",
@@ -63,7 +63,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:ListUserPolicies", "admin:ListUserPolicies",
}, },
}, },
want: 5, want: 6,
}, },
{ {
name: "all admin endpoints", name: "all admin endpoints",
@@ -72,7 +72,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:*", "admin:*",
}, },
}, },
want: 16, want: 15,
}, },
{ {
name: "all s3 endpoints", name: "all s3 endpoints",
@@ -81,7 +81,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*", "s3:*",
}, },
}, },
want: 7, want: 8,
}, },
{ {
name: "all admin and s3 endpoints", name: "all admin and s3 endpoints",
@@ -91,7 +91,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*", "s3:*",
}, },
}, },
want: 19, want: 18,
}, },
{ {
name: "no endpoints", name: "no endpoints",

222
pkg/certs/certs.go Normal file
View File

@@ -0,0 +1,222 @@
// 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 certs
import (
"context"
"crypto/x509"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/minio/cli"
"github.com/minio/minio/cmd/config"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/certs"
"github.com/mitchellh/go-homedir"
)
type GetCertificateFunc = certs.GetCertificateFunc
// ConfigDir - points to a user set directory.
type ConfigDir struct {
Path string
}
// Get - returns current directory.
func (dir *ConfigDir) Get() string {
return dir.Path
}
func getDefaultConfigDir() string {
homeDir, err := homedir.Dir()
if err != nil {
return ""
}
return filepath.Join(homeDir, DefaultConsoleConfigDir)
}
func getDefaultCertsDir() string {
return filepath.Join(getDefaultConfigDir(), CertsDir)
}
func getDefaultCertsCADir() string {
return filepath.Join(getDefaultCertsDir(), CertsCADir)
}
// isFile - returns whether given Path is a file or not.
func isFile(path string) bool {
if fi, err := os.Stat(path); err == nil {
return fi.Mode().IsRegular()
}
return false
}
var (
// DefaultCertsDir certs directory.
DefaultCertsDir = &ConfigDir{Path: getDefaultCertsDir()}
// DefaultCertsCADir CA directory.
DefaultCertsCADir = &ConfigDir{Path: getDefaultCertsCADir()}
// GlobalCertsDir points to current certs directory set by user with --certs-dir
GlobalCertsDir = DefaultCertsDir
// GlobalCertsCADir points to relative Path to certs directory and is <value-of-certs-dir>/CAs
GlobalCertsCADir = DefaultCertsCADir
)
// MkdirAllIgnorePerm attempts to create all directories, ignores any permission denied errors.
func MkdirAllIgnorePerm(path string) error {
err := os.MkdirAll(path, 0700)
if err != nil {
// It is possible in kubernetes like deployments this directory
// is already mounted and is not writable, ignore any write errors.
if os.IsPermission(err) {
err = nil
}
}
return err
}
func NewConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) (*ConfigDir, bool) {
var dir string
var dirSet bool
switch {
case ctx.IsSet(option):
dir = ctx.String(option)
dirSet = true
case ctx.GlobalIsSet(option):
dir = ctx.GlobalString(option)
dirSet = true
// cli package does not expose parent's option option. Below code is workaround.
if dir == "" || dir == getDefaultDir() {
dirSet = false // Unset to false since GlobalIsSet() true is a false positive.
if ctx.Parent().GlobalIsSet(option) {
dir = ctx.Parent().GlobalString(option)
dirSet = true
}
}
default:
// Neither local nor global option is provided. In this case, try to use
// default directory.
dir = getDefaultDir()
if dir == "" {
logger.FatalIf(errors.New("invalid arguments specified"), "%s option must be provided", option)
}
}
if dir == "" {
logger.FatalIf(errors.New("empty directory"), "%s directory cannot be empty", option)
}
// Disallow relative paths, figure out absolute paths.
dirAbs, err := filepath.Abs(dir)
logger.FatalIf(err, "Unable to fetch absolute path for %s=%s", option, dir)
logger.FatalIf(MkdirAllIgnorePerm(dirAbs), "Unable to create directory specified %s=%s", option, dir)
return &ConfigDir{Path: dirAbs}, dirSet
}
func getPublicCertFile() string {
return filepath.Join(GlobalCertsDir.Get(), PublicCertFile)
}
func getPrivateKeyFile() string {
return filepath.Join(GlobalCertsDir.Get(), PrivateKeyFile)
}
func GetTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, err error) {
ctx := context.Background()
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
return nil, nil, nil
}
if x509Certs, err = config.ParsePublicCertFile(getPublicCertFile()); err != nil {
return nil, nil, err
}
manager, err = certs.NewManager(ctx, getPublicCertFile(), getPrivateKeyFile(), config.LoadX509KeyPair)
if err != nil {
return nil, nil, err
}
//Console has support for multiple certificates. It expects the following structure:
// certs/
// │
// ├─ public.crt
// ├─ private.key
// │
// ├─ example.com/
// │ │
// │ ├─ public.crt
// │ └─ private.key
// └─ foobar.org/
// │
// ├─ public.crt
// └─ private.key
// ...
//
//Therefore, we read all filenames in the cert directory and check
//for each directory whether it contains a public.crt and private.key.
// If so, we try to add it to certificate manager.
root, err := os.Open(GlobalCertsDir.Get())
if err != nil {
return nil, nil, err
}
defer root.Close()
files, err := root.Readdir(-1)
if err != nil {
return nil, nil, err
}
for _, file := range files {
// Ignore all
// - regular files
// - "CAs" directory
// - any directory which starts with ".."
if file.Mode().IsRegular() || file.Name() == "CAs" || strings.HasPrefix(file.Name(), "..") {
continue
}
if file.Mode()&os.ModeSymlink == os.ModeSymlink {
file, err = os.Stat(filepath.Join(root.Name(), file.Name()))
if err != nil {
// not accessible ignore
continue
}
if !file.IsDir() {
continue
}
}
var (
certFile = filepath.Join(root.Name(), file.Name(), PublicCertFile)
keyFile = filepath.Join(root.Name(), file.Name(), PrivateKeyFile)
)
if !isFile(certFile) || !isFile(keyFile) {
continue
}
if err = manager.AddCertificate(certFile, keyFile); err != nil {
err = fmt.Errorf("unable to load TLS certificate '%s,%s': %w", certFile, keyFile, err)
logger.LogIf(ctx, err, logger.Application)
}
}
return x509Certs, manager, nil
}

34
pkg/certs/const.go Normal file
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/>.
package certs
const (
// Default minio configuration directory where below configuration files/directories are stored.
DefaultConsoleConfigDir = ".console"
// Directory contains below files/directories for HTTPS configuration.
CertsDir = "certs"
// Directory contains all CA certificates other than system defaults for HTTPS.
CertsCADir = "CAs"
// Public certificate file for HTTPS.
PublicCertFile = "public.crt"
// Private key file for HTTPS.
PrivateKeyFile = "private.key"
)

File diff suppressed because one or more lines are too long

View File

@@ -28,7 +28,6 @@
"local-storage-fallback": "^4.1.1", "local-storage-fallback": "^4.1.1",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"moment": "^2.24.0", "moment": "^2.24.0",
"npm": "^6.14.4",
"react": "^16.13.1", "react": "^16.13.1",
"react-app-rewire-hot-loader": "^2.0.1", "react-app-rewire-hot-loader": "^2.0.1",
"react-app-rewired": "^2.1.6", "react-app-rewired": "^2.1.6",
@@ -40,7 +39,7 @@
"react-moment": "^0.9.7", "react-moment": "^0.9.7",
"react-redux": "^7.1.3", "react-redux": "^7.1.3",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-scripts": "3.4.1", "react-scripts": "3.4.4",
"recharts": "^1.8.5", "recharts": "^1.8.5",
"redux": "^4.0.4", "redux": "^4.0.4",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11.174" height="11" viewBox="0 0 11.174 11">
<defs>
<style>.a{fill:none;stroke:#081c42;stroke-linecap:round;}</style>
</defs>
<path class="a" d="M8.392,10H1.608L0,0H10Z" transform="translate(0.587 0.5)"/>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11.174" height="11" viewBox="0 0 11.174 11">
<defs>
<style>.a{fill:#081c42;stroke:#081c42;stroke-linecap:round;}</style>
</defs>
<path class="a" d="M8.392,10H1.608L0,0H10Z" transform="translate(0.587 0.5)"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11.442" height="15.302" viewBox="0 0 11.442 15.302">
<defs>
<style>.a,.b{fill:none;stroke:#081c42;}.b{stroke-linejoin:round;}</style>
</defs>
<g transform="translate(0.5 0.5)">
<path class="a" d="M-12060-11667.842v14.261h10.442v-10.591l-3.671-3.67Z"
transform="translate(12059.999 11667.883)"/>
<path class="b" d="M-12051.353-11664.255v-3.639l3.528,3.639Z" transform="translate(12058.188 11667.894)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 515 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11.442" height="15.302" viewBox="0 0 11.442 15.302">
<defs>
<style>.a,.b{fill:#081c42;stroke:#081c42;}.b{stroke-linejoin:round;fill:#fff}</style>
</defs>
<g transform="translate(0.5 0.5)">
<path class="a" d="M-12060-11667.842v14.261h10.442v-10.591l-3.671-3.67Z"
transform="translate(12059.999 11667.883)"/>
<path class="b" d="M-12051.353-11664.255v-3.639l3.528,3.639Z" transform="translate(12058.188 11667.894)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 527 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15.999" height="13.999" viewBox="0 0 15.999 13.999">
<defs>
<style>.a{fill:none;stroke-linecap:square;}.b,.c{stroke:none;}.c{fill:#081c42;}</style>
</defs>
<g class="a" transform="translate(-0.001 0.001)">
<path class="b" d="M0,14V0H8.572V2.411H16V14Z"/>
<path class="c"
d="M 15.00020027160645 12.99860000610352 L 15.00020027160645 3.411099910736084 L 8.571599960327148 3.411099910736084 L 7.571600437164307 3.411099910736084 L 7.571600437164307 2.411099910736084 L 7.571600437164307 0.9990998506546021 L 1.000900268554688 0.9990998506546021 L 1.000900268554688 2.411099910736084 L 1.000900268554688 12.99860000610352 L 15.00020027160645 12.99860000610352 M 16.00020027160645 13.99860000610352 L 0.0009002700680866838 13.99860000610352 L 0.0009002700680866838 2.411099910736084 L 0.0009002700680866838 -0.0009001312428154051 L 8.571599960327148 -0.0009001312428154051 L 8.571599960327148 2.411099910736084 L 16.00020027160645 2.411099910736084 L 16.00020027160645 13.99860000610352 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15.999" height="13.999" viewBox="0 0 15.999 13.999">
<defs>
<style>.a{fill:none;stroke-linecap:square;}.b,.c{stroke:none;fill:#081c42}.c{fill:#081c42;}</style>
</defs>
<g class="a" transform="translate(-0.001 0.001)">
<path class="b" d="M0,14V0H8.572V2.411H16V14Z"/>
<path class="c"
d="M 15.00020027160645 12.99860000610352 L 15.00020027160645 3.411099910736084 L 8.571599960327148 3.411099910736084 L 7.571600437164307 3.411099910736084 L 7.571600437164307 2.411099910736084 L 7.571600437164307 0.9990998506546021 L 1.000900268554688 0.9990998506546021 L 1.000900268554688 2.411099910736084 L 1.000900268554688 12.99860000610352 L 15.00020027160645 12.99860000610352 M 16.00020027160645 13.99860000610352 L 0.0009002700680866838 13.99860000610352 L 0.0009002700680866838 2.411099910736084 L 0.0009002700680866838 -0.0009001312428154051 L 8.571599960327148 -0.0009001312428154051 L 8.571599960327148 2.411099910736084 L 16.00020027160645 2.411099910736084 L 16.00020027160645 13.99860000610352 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -15,17 +15,10 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react"; import React from "react";
import { import { Redirect, Route, Router, Switch } from "react-router-dom";
Redirect,
Route,
Router,
Switch,
BrowserRouter,
} from "react-router-dom";
import history from "./history"; import history from "./history";
import Login from "./screens/LoginPage/LoginPage"; import Login from "./screens/LoginPage/LoginPage";
import Console from "./screens/Console/Console"; import Console from "./screens/Console/Console";
import NotFoundPage from "./screens/NotFoundPage";
import storage from "local-storage-fallback"; import storage from "local-storage-fallback";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { AppState } from "./store"; import { AppState } from "./store";

View File

@@ -15,7 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import storage from "local-storage-fallback"; import storage from "local-storage-fallback";
import { ICapacity, IStorageType, IZoneModel } from "./types"; import { ICapacity, IZoneModel } from "./types";
const minStReq = 1073741824; // Minimal Space required for MinIO const minStReq = 1073741824; // Minimal Space required for MinIO
const minMemReq = 2147483648; // Minimal Memory required for MinIO in bytes const minMemReq = 2147483648; // Minimal Memory required for MinIO in bytes

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server // 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 // 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 // it under the terms of the GNU Affero General Public License as published by
@@ -15,21 +15,36 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react"; import React from "react";
import {SvgIcon} from "@material-ui/core"; import { SvgIcon } from "@material-ui/core";
class CreateIcon extends React.Component { class CreateIcon extends React.Component {
render() { render() {
return (<SvgIcon> return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> <SvgIcon>
<title>ic_h_create-new_sl</title> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<g id="Layer_2" data-name="Layer 2"> <g
<g id="Layer_1-2" data-name="Layer 1"> id="Group_55"
<path className="cls-1" data-name="Group 55"
d="M0,0V16H16V0ZM11.886,9.048H9.048v2.838h-2.1V9.048H4.114v-2.1H6.952V4.114h2.1V6.952h2.838Z"/> transform="translate(1002 -2555)"
</g> >
</g> <rect
</svg> id="Rectangle_29"
</SvgIcon>) width="2"
} height="12"
transform="translate(-997 2555)"
fill="#fff"
/>
<rect
id="Rectangle_30"
width="2"
height="12"
transform="translate(-990 2560) rotate(90)"
fill="#fff"
/>
</g>
</svg>
</SvgIcon>
);
}
} }
export default CreateIcon; export default CreateIcon;

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 UploadFile extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 12.996">
<g transform="translate(-63.686 -70.783)">
<path
className="a"
d="M74.736,79.879v1.95h-9.1v-1.95h-1.95v3.9h13v-3.9Z"
/>
<path
className="a"
d="M69.211,80.533h1.95V73.861h1.525l-2.5-3.078-2.5,3.078h1.525Z"
/>
</g>
</svg>
</SvgIcon>
);
}
}
export default UploadFile;

View File

@@ -23,7 +23,6 @@ import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import api from "../../../../common/api"; import api from "../../../../common/api";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper"; import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import CheckboxWrapper from "../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper"; import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { factorForDropdown, getBytes } from "../../../../common/utils"; import { factorForDropdown, getBytes } from "../../../../common/utils";
import { AppState } from "../../../../store"; import { AppState } from "../../../../store";
@@ -68,7 +67,7 @@ const styles = (theme: Theme) =>
interface IAddBucketProps { interface IAddBucketProps {
classes: any; classes: any;
open: boolean; open: boolean;
closeModalAndRefresh: () => void; closeModalAndRefresh: (refresh: boolean) => void;
addBucketName: typeof addBucketName; addBucketName: typeof addBucketName;
addBucketVersioned: typeof addBucketVersioned; addBucketVersioned: typeof addBucketVersioned;
addBucketQuota: typeof addBucketQuota; addBucketQuota: typeof addBucketQuota;
@@ -131,7 +130,7 @@ const AddBucket = ({
.then((res) => { .then((res) => {
setAddLoading(false); setAddLoading(false);
setAddError(""); setAddError("");
closeModalAndRefresh(); closeModalAndRefresh(true);
}) })
.catch((err) => { .catch((err) => {
setAddLoading(false); setAddLoading(false);
@@ -143,7 +142,7 @@ const AddBucket = ({
useEffect(() => { useEffect(() => {
addBucketName(value); addBucketName(value);
}, [value]); }, [value, addBucketName]);
const resetForm = () => { const resetForm = () => {
setBName(""); setBName("");
@@ -176,7 +175,7 @@ const AddBucket = ({
modalOpen={open} modalOpen={open}
onClose={() => { onClose={() => {
setAddError(""); setAddError("");
closeModalAndRefresh(); closeModalAndRefresh(false);
}} }}
aria-labelledby="alert-dialog-title" aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"

View File

@@ -18,7 +18,6 @@ import React, { useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import { Button } from "@material-ui/core"; import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment"; import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search";
@@ -93,7 +92,6 @@ const ListBuckets = ({
const [totalRecords, setTotalRecords] = useState<number>(0); const [totalRecords, setTotalRecords] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [deleteError, setDeleteError] = useState<string>("");
const [page, setPage] = useState<number>(0); const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10); const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false); const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
@@ -136,10 +134,13 @@ const ListBuckets = ({
} }
}, [loading, page, rowsPerPage]); }, [loading, page, rowsPerPage]);
const closeAddModalAndRefresh = () => { const closeAddModalAndRefresh = (refresh: boolean) => {
addBucketOpen(false); addBucketOpen(false);
addBucketReset(); addBucketReset();
setLoading(true);
if (refresh) {
setLoading(true);
}
}; };
const closeDeleteModalAndRefresh = (refresh: boolean) => { const closeDeleteModalAndRefresh = (refresh: boolean) => {
@@ -184,28 +185,26 @@ const ListBuckets = ({
return <Moment>{date}</Moment>; return <Moment>{date}</Moment>;
}; };
const filteredRecords = records const filteredRecords = records.filter((b: Bucket) => {
.filter((b: Bucket) => { if (filterBuckets === "") {
if (filterBuckets === "") { return true;
} else {
if (b.name.indexOf(filterBuckets) >= 0) {
return true; return true;
} else { } else {
if (b.name.indexOf(filterBuckets) >= 0) { return false;
return true;
} else {
return false;
}
} }
}) }
.slice(offset, offset + rowsPerPage); });
const showInPage = filteredRecords.slice(offset, offset + rowsPerPage);
return ( return (
<React.Fragment> <React.Fragment>
{addBucketModalOpen && ( {addBucketModalOpen && (
<AddBucket <AddBucket
open={addBucketModalOpen} open={addBucketModalOpen}
closeModalAndRefresh={() => { closeModalAndRefresh={closeAddModalAndRefresh}
closeAddModalAndRefresh();
}}
/> />
)} )}
{deleteOpen && ( {deleteOpen && (
@@ -219,6 +218,7 @@ const ListBuckets = ({
)} )}
<PageHeader label={"Buckets"} /> <PageHeader label={"Buckets"} />
<Grid container> <Grid container>
{error !== "" && <span className={classes.error}>{error}</span>}
<Grid item xs={12} className={classes.container}> <Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}> <Grid item xs={12} className={classes.actionsTray}>
<TextField <TextField
@@ -269,13 +269,13 @@ const ListBuckets = ({
}, },
]} ]}
isLoading={loading} isLoading={loading}
records={filteredRecords} records={showInPage}
entityName="Buckets" entityName="Buckets"
idField="name" idField="name"
paginatorConfig={{ paginatorConfig={{
rowsPerPageOptions: [5, 10, 25], rowsPerPageOptions: [5, 10, 25],
colSpan: 3, colSpan: 3,
count: totalRecords, count: filteredRecords.length,
rowsPerPage: rowsPerPage, rowsPerPage: rowsPerPage,
page: page, page: page,
SelectProps: { SelectProps: {

View File

@@ -0,0 +1,116 @@
// 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 ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
import { Button, Grid, LinearProgress } from "@material-ui/core";
import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../../../Common/FormComponents/common/styleLibrary";
import { connect } from "react-redux";
import { createFolder } from "../../../../ObjectBrowser/actions";
interface ICreateFolder {
classes: any;
modalOpen: boolean;
folderName: string;
createFolder: (newFolder: string) => any;
onClose: () => any;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
pathLabel: {
marginTop: 0,
marginBottom: 32,
},
...modalBasic,
});
const CreateFolderModal = ({
modalOpen,
folderName,
onClose,
createFolder,
classes,
}: ICreateFolder) => {
const [pathUrl, setPathUrl] = useState("");
const resetForm = () => {
setPathUrl("");
};
const createProcess = () => {
createFolder(pathUrl);
onClose();
};
const folderTruncated = folderName.split("/").slice(2).join("/");
return (
<React.Fragment>
<ModalWrapper modalOpen={modalOpen} title="Add Folder" onClose={onClose}>
<Grid container>
<h3 className={classes.pathLabel}>
Current Path: {folderTruncated}/
</h3>
<Grid item xs={12}>
<InputBoxWrapper
value={pathUrl}
label={"Folder Path"}
id={"folderPath"}
name={"folderPath"}
placeholder={"Enter Folder Path"}
onChange={(e) => {
setPathUrl(e.target.value);
}}
/>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<button
type="button"
color="primary"
className={classes.clearButton}
onClick={resetForm}
>
Clear
</button>
<Button
type="submit"
variant="contained"
color="primary"
disabled={pathUrl.trim() === ""}
onClick={createProcess}
>
Save
</Button>
</Grid>
</Grid>
</ModalWrapper>
</React.Fragment>
);
};
const mapDispatchToProps = {
createFolder,
};
const connector = connect(null, mapDispatchToProps);
export default connector(withStyles(styles)(CreateFolderModal));

View File

@@ -14,6 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
@@ -21,7 +22,6 @@ import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search";
import { BucketObject, BucketObjectsList } from "./types"; import { BucketObject, BucketObjectsList } from "./types";
import api from "../../../../../../common/api"; import api from "../../../../../../common/api";
import React from "react";
import TableWrapper from "../../../../Common/TableWrapper/TableWrapper"; import TableWrapper from "../../../../Common/TableWrapper/TableWrapper";
import { niceBytes } from "../../../../../../common/utils"; import { niceBytes } from "../../../../../../common/utils";
import DeleteObject from "./DeleteObject"; import DeleteObject from "./DeleteObject";
@@ -29,6 +29,7 @@ import DeleteObject from "./DeleteObject";
import { import {
actionsTray, actionsTray,
containerForHeader, containerForHeader,
objectBrowserCommon,
searchField, searchField,
} from "../../../../Common/FormComponents/common/styleLibrary"; } from "../../../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../../../Common/PageHeader/PageHeader"; import PageHeader from "../../../../Common/PageHeader/PageHeader";
@@ -38,6 +39,23 @@ import { Button, Input } from "@material-ui/core";
import * as reactMoment from "react-moment"; import * as reactMoment from "react-moment";
import { CreateIcon } from "../../../../../../icons"; import { CreateIcon } from "../../../../../../icons";
import Snackbar from "@material-ui/core/Snackbar"; import Snackbar from "@material-ui/core/Snackbar";
import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs";
import get from "lodash/get";
import { withRouter } from "react-router-dom";
import { addRoute, setAllRoutes } from "../../../../ObjectBrowser/actions";
import { connect } from "react-redux";
import { ObjectBrowserState, Route } from "../../../../ObjectBrowser/reducers";
import CreateFolderModal from "./CreateFolderModal";
import { create } from "domain";
import UploadFile from "../../../../../../icons/UploadFile";
const commonIcon = {
backgroundRepeat: "no-repeat",
backgroundPosition: "center center",
width: 16,
height: 40,
marginRight: 10,
};
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@@ -69,93 +87,125 @@ const styles = (theme: Theme) =>
}, },
}, },
}, },
fileName: {
display: "flex",
alignItems: "center",
},
iconFolder: {
backgroundImage: "url(/images/ob_folder_clear.svg)",
...commonIcon,
},
iconFile: {
backgroundImage: "url(/images/ob_file_clear.svg)",
...commonIcon,
},
buttonsContainer: {
"& .MuiButtonBase-root": {
marginLeft: 10,
},
},
"@global": {
".rowElementRaw:hover .iconFileElm": {
backgroundImage: "url(/images/ob_file_filled.svg)",
},
".rowElementRaw:hover .iconFolderElm": {
backgroundImage: "url(/images/ob_folder_filled.svg)",
},
},
...actionsTray, ...actionsTray,
...searchField, ...searchField,
...objectBrowserCommon,
...containerForHeader(theme.spacing(4)), ...containerForHeader(theme.spacing(4)),
}); });
interface IListObjectsProps { interface IListObjectsProps {
classes: any; classes: any;
match: any; match: any;
addRoute: (param1: string, param2: string) => any;
setAllRoutes: (path: string) => any;
routesList: Route[];
} }
interface IListObjectsState { interface ObjectBrowserReducer {
records: BucketObject[]; objectBrowser: ObjectBrowserState;
totalRecords: number;
loading: boolean;
error: string;
deleteOpen: boolean;
deleteError: string;
selectedObject: string;
selectedBucket: string;
filterObjects: string;
openSnackbar: boolean;
snackBarMessage: string;
} }
class ListObjects extends React.Component< const ListObjects = ({
IListObjectsProps, classes,
IListObjectsState match,
> { addRoute,
state: IListObjectsState = { setAllRoutes,
records: [], routesList,
totalRecords: 0, }: IListObjectsProps) => {
loading: false, const [records, setRecords] = useState<BucketObject[]>([]);
error: "", const [totalRecords, setTotalRecords] = useState<number>(0);
deleteOpen: false, const [loading, setLoading] = useState<boolean>(true);
deleteError: "", const [error, setError] = useState<string>("");
selectedObject: "", const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
selectedBucket: "", const [createFolderOpen, setCreateFolderOpen] = useState<boolean>(false);
filterObjects: "", const [deleteError, setDeleteError] = useState<string>("");
openSnackbar: false, const [selectedObject, setSelectedObject] = useState<string>("");
snackBarMessage: "", const [selectedBucket, setSelectedBucket] = useState<string>("");
const [filterObjects, setFilterObjects] = useState<string>("");
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
const [snackBarMessage, setSnackbarMessage] = useState<string>("");
useEffect(() => {
const bucketName = match.params["bucket"];
const internalPaths = match.params[0];
let extraPath = "";
if (internalPaths) {
extraPath = `?prefix=${internalPaths}/`;
}
api
.invoke("GET", `/api/v1/buckets/${bucketName}/objects${extraPath}`)
.then((res: BucketObjectsList) => {
setLoading(false);
setSelectedBucket(bucketName);
setRecords(res.objects || []);
setTotalRecords(!res.objects ? 0 : res.total);
setError("");
// TODO:
// if we get 0 results, and page > 0 , go down 1 page
})
.catch((err: any) => {
setLoading(false);
setError(err);
});
}, [loading, match]);
useEffect(() => {
const url = get(match, "url", "/object-browser");
if (url !== routesList[routesList.length - 1].route) {
setAllRoutes(url);
}
}, [match, routesList, setAllRoutes]);
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
setLoading(true);
}
}; };
fetchRecords = () => { const closeAddFolderModal = () => {
this.setState({ loading: true }, () => { setCreateFolderOpen(false);
const { match } = this.props;
const bucketName = match.params["bucket"];
api
.invoke("GET", `/api/v1/buckets/${bucketName}/objects`)
.then((res: BucketObjectsList) => {
this.setState({
loading: false,
selectedBucket: bucketName,
records: res.objects || [],
totalRecords: !res.objects ? 0 : res.total,
error: "",
});
// TODO:
// if we get 0 results, and page > 0 , go down 1 page
})
.catch((err: any) => {
this.setState({ loading: false, error: err });
});
});
}; };
componentDidMount(): void { const showSnackBarMessage = (text: string) => {
this.fetchRecords(); setSnackbarMessage(text);
} setOpenSnackbar(true);
};
closeDeleteModalAndRefresh(refresh: boolean) { const closeSnackBar = () => {
this.setState({ deleteOpen: false }, () => { setSnackbarMessage("");
if (refresh) { setOpenSnackbar(false);
this.fetchRecords(); };
}
});
}
showSnackBarMessage(text: string) { const upload = (e: any, bucketName: string, path: string) => {
this.setState({ openSnackbar: true, snackBarMessage: text });
}
closeSnackBar() {
this.setState({ openSnackbar: false, snackBarMessage: `` });
}
upload(e: any, bucketName: string, path: string) {
let listObjects = this;
if (isNullOrUndefined(e) || isNullOrUndefined(e.target)) { if (isNullOrUndefined(e) || isNullOrUndefined(e.target)) {
return; return;
} }
@@ -174,25 +224,21 @@ class ListObjects extends React.Component<
xhr.withCredentials = false; xhr.withCredentials = false;
xhr.onload = function (event) { xhr.onload = function (event) {
// TODO: handle status // TODO: handle status
if (xhr.status == 401 || xhr.status == 403) { if (xhr.status === 401 || xhr.status === 403) {
listObjects.showSnackBarMessage( showSnackBarMessage("An error occurred while uploading the file.");
"An error occurred while uploading the file."
);
} }
if (xhr.status == 500) { if (xhr.status === 500) {
listObjects.showSnackBarMessage( showSnackBarMessage("An error occurred while uploading the file.");
"An error occurred while uploading the file."
);
} }
if (xhr.status == 200) { if (xhr.status === 200) {
listObjects.showSnackBarMessage("Object uploaded successfully."); showSnackBarMessage("Object uploaded successfully.");
listObjects.fetchRecords(); setLoading(true);
} }
}; };
xhr.upload.addEventListener("error", (event) => { xhr.upload.addEventListener("error", (event) => {
// TODO: handle error // TODO: handle error
this.showSnackBarMessage("An error occurred while uploading the file."); showSnackBarMessage("An error occurred while uploading the file.");
}); });
xhr.upload.addEventListener("progress", (event) => { xhr.upload.addEventListener("progress", (event) => {
@@ -200,24 +246,22 @@ class ListObjects extends React.Component<
}); });
xhr.onerror = () => { xhr.onerror = () => {
listObjects.showSnackBarMessage( showSnackBarMessage("An error occurred while uploading the file.");
"An error occurred while uploading the file."
);
}; };
var formData = new FormData(); const formData = new FormData();
var blobFile = new Blob([file]); const blobFile = new Blob([file]);
formData.append("upfile", blobFile); formData.append("upfile", blobFile);
xhr.send(formData); xhr.send(formData);
e.target.value = null; e.target.value = null;
} };
download(bucketName: string, objectName: string) { const download = (bucketName: string, objectName: string) => {
var anchor = document.createElement("a"); const anchor = document.createElement("a");
document.body.appendChild(anchor); document.body.appendChild(anchor);
const token: string = storage.getItem("token")!; const token: string = storage.getItem("token")!;
var xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open( xhr.open(
"GET", "GET",
@@ -228,11 +272,11 @@ class ListObjects extends React.Component<
xhr.responseType = "blob"; xhr.responseType = "blob";
xhr.onload = function (e) { xhr.onload = function (e) {
if (this.status == 200) { if (this.status === 200) {
var blob = new Blob([this.response], { const blob = new Blob([this.response], {
type: "octet/stream", type: "octet/stream",
}); });
var blobUrl = window.URL.createObjectURL(blob); const blobUrl = window.URL.createObjectURL(blob);
anchor.href = blobUrl; anchor.href = blobUrl;
anchor.download = objectName; anchor.download = objectName;
@@ -243,158 +287,228 @@ class ListObjects extends React.Component<
} }
}; };
xhr.send(); xhr.send();
} };
bucketFilter(): void {} const displayParsedDate = (date: string) => {
return <reactMoment.default>{date}</reactMoment.default>;
};
render() { const confirmDeleteObject = (object: string) => {
const { classes } = this.props; setDeleteOpen(true);
const { setSelectedObject(object);
records, };
loading,
selectedObject,
selectedBucket,
deleteOpen,
filterObjects,
snackBarMessage,
openSnackbar,
} = this.state;
const displayParsedDate = (date: string) => {
return <reactMoment.default>{date}</reactMoment.default>;
};
const confirmDeleteObject = (object: string) => { const downloadObject = (object: string) => {
this.setState({ deleteOpen: true, selectedObject: object }); download(selectedBucket, object);
}; };
const downloadObject = (object: string) => { const openPath = (idElement: string) => {
this.download(selectedBucket, object); const currentPath = get(match, "url", "/object-browser");
};
const uploadObject = (e: any): void => { // Element is a folder, we redirect to it
// TODO: handle deeper paths/folders if (idElement.endsWith("/")) {
let file = e.target.files[0]; const idElementClean = idElement
this.showSnackBarMessage(`Uploading: ${file.name}`); .substr(0, idElement.length - 1)
this.upload(e, selectedBucket, ""); .split("/");
}; const lastIndex = idElementClean.length - 1;
const newPath = `${currentPath}/${idElementClean[lastIndex]}`;
const snackBarAction = ( addRoute(newPath, idElementClean[lastIndex]);
<Button return;
color="secondary" }
size="small"
onClick={() => {
this.closeSnackBar();
}}
>
Dismiss
</Button>
);
const tableActions = [ // Element is a file. we open details here
{ type: "download", onClick: downloadObject, sendOnlyId: true }, // TODO: Add details open function here.
{ type: "delete", onClick: confirmDeleteObject, sendOnlyId: true }, //console.log("object", idElementClean);
]; };
const filteredRecords = records.filter((b: BucketObject) => { const uploadObject = (e: any): void => {
if (filterObjects === "") { // Handle of deeper routes.
return true; const currentPath = routesList[routesList.length - 1].route;
} else { const splitPaths = currentPath
if (b.name.indexOf(filterObjects) >= 0) { .split("/")
return true; .filter((item) => item.trim() !== "");
} else {
return false; let path = "";
}
} if (splitPaths.length > 2) {
}); path = `${splitPaths.slice(2).join("/")}/`;
}
let file = e.target.files[0];
showSnackBarMessage(`Uploading: ${file.name}`);
upload(e, selectedBucket, path);
};
const snackBarAction = (
<Button
color="secondary"
size="small"
onClick={() => {
closeSnackBar();
}}
>
Dismiss
</Button>
);
const tableActions = [
{ type: "view", onClick: openPath, sendOnlyId: true },
{ type: "download", onClick: downloadObject, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteObject, sendOnlyId: true },
];
const displayName = (element: string) => {
let elementString = element;
let icon = `${classes.iconFile} iconFileElm`;
// Element is a folder
if (element.endsWith("/")) {
icon = `${classes.iconFolder} iconFolderElm`;
elementString = element.substr(0, element.length - 1);
}
const splitItem = elementString.split("/");
return ( return (
<React.Fragment> <div className={classes.fileName}>
{deleteOpen && ( <div className={icon} />
<DeleteObject <span>{splitItem[splitItem.length - 1]}</span>
deleteOpen={deleteOpen} </div>
selectedBucket={selectedBucket} );
selectedObject={selectedObject} };
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
)}
<Snackbar
open={openSnackbar}
message={snackBarMessage}
action={snackBarAction}
/>
<PageHeader label="Objects" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Objects"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
this.setState({
filterObjects: val.target.value,
});
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<> const filteredRecords = records.filter((b: BucketObject) => {
<Button if (filterObjects === "") {
variant="contained" return true;
color="primary" } else {
startIcon={<CreateIcon />} if (b.name.indexOf(filterObjects) >= 0) {
component="label" return true;
> } else {
Upload Object return false;
<Input }
type="file" }
onChange={(e) => uploadObject(e)} });
id="file-input"
style={{ display: "none" }} return (
/> <React.Fragment>
</Button> {deleteOpen && (
</> <DeleteObject
</Grid> deleteOpen={deleteOpen}
<Grid item xs={12}> selectedBucket={selectedBucket}
<br /> selectedObject={selectedObject}
</Grid> closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
<Grid item xs={12}> />
<TableWrapper )}
itemActions={tableActions} {createFolderOpen && (
columns={[ <CreateFolderModal
{ label: "Name", elementKey: "name" }, modalOpen={createFolderOpen}
{ folderName={routesList[routesList.length - 1].route}
label: "Last Modified", onClose={closeAddFolderModal}
elementKey: "last_modified", />
renderFunction: displayParsedDate, )}
}, <Snackbar
{ open={openSnackbar}
label: "Size", message={snackBarMessage}
elementKey: "size", action={snackBarAction}
renderFunction: niceBytes, />
}, <PageHeader label="Object Browser" />
]} <Grid container>
isLoading={loading} <Grid item xs={12} className={classes.container}>
entityName="Objects" <Grid item xs={12} className={classes.obTitleSection}>
idField="name" <div>
records={filteredRecords} <BrowserBreadcrumbs />
/> </div>
</Grid> <div className={classes.buttonsContainer}>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
component="label"
onClick={() => {
setCreateFolderOpen(true);
}}
>
Create Folder
</Button>
<Button
variant="contained"
color="primary"
startIcon={<UploadFile />}
component="label"
>
File
<Input
type="file"
onChange={(e) => uploadObject(e)}
id="file-input"
style={{ display: "none" }}
/>
</Button>
</div>
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Objects"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterObjects(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{
label: "Name",
elementKey: "name",
renderFunction: displayName,
},
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
},
{
label: "Size",
elementKey: "size",
renderFunction: niceBytes,
},
]}
isLoading={loading}
entityName="Objects"
idField="name"
records={filteredRecords}
/>
</Grid> </Grid>
</Grid> </Grid>
</React.Fragment> </Grid>
); </React.Fragment>
} );
} };
export default withStyles(styles)(ListObjects); const mapStateToProps = ({ objectBrowser }: ObjectBrowserReducer) => ({
routesList: get(objectBrowser, "routesList", []),
});
const mapDispatchToProps = {
addRoute,
setAllRoutes,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default withRouter(connector(withStyles(styles)(ListObjects)));

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react"; import React, { useState } from "react";
import get from "lodash/get"; import get from "lodash/get";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper"; import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
@@ -22,7 +22,6 @@ import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { Button, LinearProgress } from "@material-ui/core"; import { Button, LinearProgress } from "@material-ui/core";
import api from "../../../../common/api"; import api from "../../../../common/api";
import { IRemoteBucket } from "../types"; import { IRemoteBucket } from "../types";

View File

@@ -0,0 +1,199 @@
// 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 Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
buttonContainer: {
textAlign: "right",
},
...modalBasic,
});
interface IEnableBucketEncryptionProps {
classes: any;
open: boolean;
selectedBucket: string;
closeModalAndRefresh: () => void;
}
interface IEnableBucketEncryptionState {
loading: boolean;
encryptionError: string;
kmsKeyID: string;
suffix: string;
encryptionType: string;
}
class EnableBucketEncryption extends React.Component<
IEnableBucketEncryptionProps,
IEnableBucketEncryptionState
> {
state: IEnableBucketEncryptionState = {
loading: false,
encryptionError: "",
kmsKeyID: "",
suffix: "",
encryptionType: "sse-s3",
};
enableBucketEncryption(event: React.FormEvent) {
event.preventDefault();
const { kmsKeyID, loading, encryptionType } = this.state;
const { selectedBucket } = this.props;
if (loading) {
return;
}
api
.invoke("POST", `/api/v1/buckets/${selectedBucket}/encryption/enable`, {
encType: encryptionType,
kmsKeyID: kmsKeyID,
})
.then(() => {
this.setState(
{
loading: false,
encryptionError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch((err: any) => {
this.setState({ encryptionError: err });
});
}
render() {
const { classes, open } = this.props;
const { loading, encryptionError, kmsKeyID, encryptionType } = this.state;
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
this.setState({ encryptionError: "" }, () => {
this.props.closeModalAndRefresh();
});
}}
title="Enable Bucket Encryption"
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
this.enableBucketEncryption(e);
}}
>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{encryptionError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{encryptionError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<SelectWrapper
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ encryptionType: e.target.value as string });
}}
id="select-encryption-type"
name="select-encryption-type"
label={"Encryption Type"}
value={encryptionType}
options={[
{
label: "SSE-S3",
value: "sse-s3",
},
{
label: "SSE-KMS",
value: "sse-kms",
},
]}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{encryptionType === "sse-kms" && (
<Grid item xs={12}>
<InputBoxWrapper
id="kms-key-id"
name="kms-key-id"
label="KMS Key ID"
value={kmsKeyID}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ kmsKeyID: e.target.value });
}}
/>
</Grid>
)}
<Grid item xs={12}>
<br />
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={loading}
>
Save
</Button>
</Grid>
{loading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
}
}
export default withStyles(styles)(EnableBucketEncryption);

View File

@@ -94,7 +94,7 @@ class SetAccessPolicy extends React.Component<
} }
render() { render() {
const { classes, open, actualPolicy } = this.props; const { classes, open } = this.props;
const { addLoading, addError, accessPolicy } = this.state; const { addLoading, addError, accessPolicy } = this.state;
return ( return (
<ModalWrapper <ModalWrapper

View File

@@ -27,6 +27,7 @@ import {
BucketEvent, BucketEvent,
BucketEventList, BucketEventList,
BucketInfo, BucketInfo,
BucketEncryptionInfo,
BucketList, BucketList,
BucketReplication, BucketReplication,
BucketReplicationDestination, BucketReplicationDestination,
@@ -34,8 +35,7 @@ import {
BucketReplicationRuleDeleteMarker, BucketReplicationRuleDeleteMarker,
BucketVersioning, BucketVersioning,
} from "../types"; } from "../types";
import { Box, Button } from "@material-ui/core"; import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import SetAccessPolicy from "./SetAccessPolicy"; import SetAccessPolicy from "./SetAccessPolicy";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions"; import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../../../icons"; import { CreateIcon } from "../../../../icons";
@@ -46,6 +46,8 @@ import { niceBytes } from "../../../../common/utils";
import AddReplicationModal from "./AddReplicationModal"; import AddReplicationModal from "./AddReplicationModal";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary"; import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../Common/PageHeader/PageHeader"; import PageHeader from "../../Common/PageHeader/PageHeader";
import Checkbox from "@material-ui/core/Checkbox";
import EnableBucketEncryption from "./EnableBucketEncryption";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@@ -191,6 +193,7 @@ interface IViewBucketState {
rowsPerPage: number; rowsPerPage: number;
curTab: number; curTab: number;
addScreenOpen: boolean; addScreenOpen: boolean;
enableEncryptionScreenOpen: boolean;
deleteOpen: boolean; deleteOpen: boolean;
selectedBucket: string; selectedBucket: string;
selectedEvent: BucketEvent | null; selectedEvent: BucketEvent | null;
@@ -199,6 +202,7 @@ interface IViewBucketState {
replicationSet: boolean; replicationSet: boolean;
openSetReplication: boolean; openSetReplication: boolean;
isVersioned: boolean; isVersioned: boolean;
encryptionEnabled: boolean;
} }
class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> { class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
@@ -218,6 +222,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
curTab: 0, curTab: 0,
rowsPerPage: 10, rowsPerPage: 10,
addScreenOpen: false, addScreenOpen: false,
enableEncryptionScreenOpen: false,
deleteOpen: false, deleteOpen: false,
selectedBucket: "", selectedBucket: "",
selectedEvent: null, selectedEvent: null,
@@ -226,6 +231,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
replicationSet: false, replicationSet: false,
openSetReplication: false, openSetReplication: false,
isVersioned: false, isVersioned: false,
encryptionEnabled: false,
}; };
fetchEvents() { fetchEvents() {
@@ -325,6 +331,21 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
}); });
} }
fetchBucketEncryptionInfo() {
const { match } = this.props;
const bucketName = match.params["bucketName"];
api
.invoke("GET", `/api/v1/buckets/${bucketName}/encryption/info`)
.then((res: BucketEncryptionInfo) => {
if (res.algorithm) {
this.setState({ encryptionEnabled: true });
}
})
.catch((err) => {
console.log(err);
});
}
closeAddModalAndRefresh() { closeAddModalAndRefresh() {
this.setState({ setAccessPolicyScreenOpen: false }, () => { this.setState({ setAccessPolicyScreenOpen: false }, () => {
this.loadInfo(); this.loadInfo();
@@ -343,6 +364,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
this.loadInfo(); this.loadInfo();
this.fetchEvents(); this.fetchEvents();
this.fetchBucketsSize(); this.fetchBucketsSize();
this.fetchBucketEncryptionInfo();
} }
bucketFilter(): void {} bucketFilter(): void {}
@@ -360,14 +382,15 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
rowsPerPage, rowsPerPage,
deleteOpen, deleteOpen,
addScreenOpen, addScreenOpen,
enableEncryptionScreenOpen,
selectedEvent, selectedEvent,
bucketSize, bucketSize,
loadingSize, loadingSize,
replicationSet,
openSetReplication, openSetReplication,
isVersioned, isVersioned,
replicationRules, replicationRules,
curTab, curTab,
encryptionEnabled,
} = this.state; } = this.state;
const offset = page * rowsPerPage; const offset = page * rowsPerPage;
@@ -415,6 +438,23 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
this.setState({ openSetReplication: open }); this.setState({ openSetReplication: open });
}; };
const handleEncryptionCheckbox = (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (event.target.checked) {
this.setState({ enableEncryptionScreenOpen: true });
} else {
api
.invoke("POST", `/api/v1/buckets/${bucketName}/encryption/disable`)
.then(() => {
this.setState({ encryptionEnabled: false });
})
.catch((err: any) => {
this.setState({ error: err });
});
}
};
const tableActions = [{ type: "delete", onClick: confirmDeleteEvent }]; const tableActions = [{ type: "delete", onClick: confirmDeleteEvent }];
const filteredRecords = records.slice(offset, offset + rowsPerPage); const filteredRecords = records.slice(offset, offset + rowsPerPage);
@@ -432,6 +472,16 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
}} }}
/> />
)} )}
{enableEncryptionScreenOpen && (
<EnableBucketEncryption
open={enableEncryptionScreenOpen}
selectedBucket={bucketName}
closeModalAndRefresh={() => {
this.setState({ enableEncryptionScreenOpen: false });
this.fetchBucketEncryptionInfo();
}}
/>
)}
{setAccessPolicyScreenOpen && ( {setAccessPolicyScreenOpen && (
<SetAccessPolicy <SetAccessPolicy
bucketName={bucketName} bucketName={bucketName}
@@ -490,6 +540,17 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
</div> </div>
<div>Versioning:</div> <div>Versioning:</div>
<div>{isVersioned ? "Yes" : "No"}&nbsp;</div> <div>{isVersioned ? "Yes" : "No"}&nbsp;</div>
<div>Encryption:</div>
<div>
<Checkbox
color="primary"
inputProps={{
"aria-label": "secondary checkbox",
}}
onChange={(event) => handleEncryptionCheckbox(event)}
checked={encryptionEnabled}
/>
</div>
</div> </div>
</Paper> </Paper>
</div> </div>

View File

@@ -19,6 +19,11 @@ export interface Bucket {
creation_date: Date; creation_date: Date;
} }
export interface BucketEncryptionInfo {
algorithm: string;
kmsMasterKeyID: string;
}
export interface BucketInfo { export interface BucketInfo {
name: string; name: string;
access: string; access: string;

View File

@@ -105,8 +105,8 @@ const CSVMultiSelector = ({
firstUpdate.current = false; firstUpdate.current = false;
return; return;
} }
debouncedOnChange(); debouncedOnChange();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentElements]); }, [currentElements]);
// If the last input is not empty, we add a new one // If the last input is not empty, we add a new one

View File

@@ -14,21 +14,8 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react"; import React from "react";
import { import { Checkbox, Grid, InputLabel, Tooltip } from "@material-ui/core";
Checkbox, import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
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 { import {
checkboxIcons, checkboxIcons,
fieldBasic, fieldBasic,
@@ -54,8 +41,14 @@ const styles = (theme: Theme) =>
...fieldBasic, ...fieldBasic,
...tooltipHelper, ...tooltipHelper,
...checkboxIcons, ...checkboxIcons,
labelContainer: { fieldContainer: {
flexGrow: 1, ...fieldBasic.fieldContainer,
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
margin: "15px 0",
marginBottom: 0,
flexBasis: "initial",
}, },
}); });
@@ -73,19 +66,7 @@ const CheckboxWrapper = ({
return ( return (
<React.Fragment> <React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}> <Grid item xs={12} className={classes.fieldContainer}>
{label !== "" && ( <div>
<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 <Checkbox
name={name} name={name}
id={id} id={id}
@@ -99,6 +80,18 @@ const CheckboxWrapper = ({
disabled={disabled} disabled={disabled}
/> />
</div> </div>
{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>
)}
</Grid> </Grid>
</React.Fragment> </React.Fragment>
); );

View File

@@ -14,20 +14,8 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react"; import React from "react";
import { import { Grid, InputLabel, TextField, Tooltip } from "@material-ui/core";
Grid, import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
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 { fieldBasic, tooltipHelper } from "../common/styleLibrary"; import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help"; import HelpIcon from "@material-ui/icons/Help";
@@ -105,12 +93,7 @@ const CommentBoxWrapper = ({
}`} }`}
> >
{label !== "" && ( {label !== "" && (
<InputLabel <InputLabel htmlFor={id} className={classes.inputLabel}>
htmlFor={id}
className={`${error !== "" ? classes.fieldLabelError : ""} ${
classes.inputLabel
}`}
>
<span> <span>
{label} {label}
{required ? "*" : ""} {required ? "*" : ""}

View File

@@ -76,6 +76,12 @@ const styles = (theme: Theme) =>
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
}, },
fieldBottom: {
borderBottom: "#9c9c9c 1px solid",
},
fileInputField: {
margin: "13px 0",
},
}); });
const FileSelector = ({ const FileSelector = ({
@@ -98,7 +104,7 @@ const FileSelector = ({
<Grid <Grid
item item
xs={12} xs={12}
className={`${classes.fieldContainer} ${ className={`${classes.fieldBottom} ${classes.fieldContainer} ${
error !== "" ? classes.errorInField : "" error !== "" ? classes.errorInField : ""
}`} }`}
> >
@@ -137,6 +143,7 @@ const FileSelector = ({
accept={accept} accept={accept}
required={required} required={required}
disabled={disabled} disabled={disabled}
className={classes.fileInputField}
/> />
{value !== "" && ( {value !== "" && (

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react"; import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { InputLabel, Switch, Tooltip } from "@material-ui/core"; import { InputLabel, Switch, Tooltip } from "@material-ui/core";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";

View File

@@ -109,7 +109,6 @@ const inputStyles = makeStyles((theme: Theme) =>
}, },
error: { error: {
color: "#b53b4b", color: "#b53b4b",
boxShadow: "inset 0px 0px 1px 1px #b53b4b",
}, },
}) })
); );
@@ -166,12 +165,7 @@ const InputBoxWrapper = ({
}`} }`}
> >
{label !== "" && ( {label !== "" && (
<InputLabel <InputLabel htmlFor={id} className={classes.inputLabel}>
htmlFor={id}
className={`${error !== "" ? classes.fieldLabelError : ""} ${
classes.inputLabel
}`}
>
<span> <span>
{label} {label}
{required ? "*" : ""} {required ? "*" : ""}

View File

@@ -145,6 +145,7 @@ export const actionsTray = {
export const searchField = { export const searchField = {
searchField: { searchField: {
flexGrow: 1, flexGrow: 1,
height: 40,
background: "#FFFFFF", background: "#FFFFFF",
borderRadius: 5, borderRadius: 5,
border: "#EAEDEE 1px solid", border: "#EAEDEE 1px solid",
@@ -178,3 +179,31 @@ export const predefinedList = {
minHeight: 41, minHeight: 41,
}, },
}; };
export const objectBrowserCommon = {
obTitleSection: {
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
marginBottom: 20,
},
sectionTitle: {
fontSize: 22,
color: "#000",
fontWeight: 600,
height: 40,
lineHeight: "40px",
},
breadcrumbs: {
fontSize: 10,
color: "#000",
marginTop: 2,
"& a": {
textDecoration: "none",
color: "#000",
"&:hover": {
textDecoration: "underline",
},
},
},
};

View File

@@ -30,7 +30,7 @@ const styles = (theme: Theme) =>
minWidth: 180, minWidth: 180,
marginRight: 10, marginRight: 10,
"& ul": { "& ul": {
padding: 15, padding: "0 15px 0 0",
"& li": { "& li": {
listStyle: "lower-roman", listStyle: "lower-roman",

View File

@@ -51,6 +51,7 @@ const WizardPage = ({ classes, page, pageChange }: IWizardPage) => {
break; break;
case "to": case "to":
pageChange(btn.toPage || 0); pageChange(btn.toPage || 0);
break;
default: default:
} }

View File

@@ -16,12 +16,7 @@
import React from "react"; import React from "react";
import { Dialog, DialogContent, DialogTitle } from "@material-ui/core"; import { Dialog, DialogContent, DialogTitle } from "@material-ui/core";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import { import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
createStyles,
makeStyles,
Theme,
withStyles,
} from "@material-ui/core/styles";
interface IModalProps { interface IModalProps {
classes: any; classes: any;

View File

@@ -7,13 +7,17 @@ const DeleteIcon = ({ active = false }: IIcon) => {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="16" width="16"
height="16" height="16"
viewBox="0 0 16 16" viewBox="0 0 10.402 13"
> >
<g transform="translate(-1225 -657)"> <g transform="translate(0.004 -28.959)">
<path <path
fill={active ? selected : unSelected} fill={active ? selected : unSelected}
d="M0,8H0a8,8,0,0,0,8,8H8a8,8,0,0,0,8-8h0A8,8,0,0,0,8,0H8A8,8,0,0,0,0,8Zm10.007,3.489L8,9.482,5.993,11.489,4.511,10.007,6.518,8,4.511,5.993,5.993,4.511,8,6.518l2.007-2.007,1.482,1.482L9.482,8l2.007,2.007Z" d="M6.757,29.959v-1H3.636v1H0v1H10.4v-1Z"
transform="translate(1225 657)" />
<path
fill={active ? selected : unSelected}
d="M0,31.957l1.672,10H8.724l1.673-10ZM3.412,40.2,2.86,33.722h.653l.553,6.472Zm3.569,0H6.328l.551-6.472h.654Z"
transform="translate(0 0)"
/> />
</g> </g>
</svg> </svg>

View File

@@ -54,6 +54,7 @@ interface IColumns {
renderFunction?: (input: any) => any; renderFunction?: (input: any) => any;
renderFullObject?: boolean; renderFullObject?: boolean;
globalClass?: any; globalClass?: any;
rowClass?: any;
} }
interface IPaginatorConfig { interface IPaginatorConfig {
@@ -89,12 +90,18 @@ interface TableWrapperProps {
paginatorConfig?: IPaginatorConfig; paginatorConfig?: IPaginatorConfig;
} }
const borderColor = "#eaeaea"; const borderColor = "#9c9c9c80";
const rowText = { const rowText = {
fontWeight: 400, fontWeight: 400,
fontSize: 14, fontSize: 14,
borderColor: borderColor, borderColor: borderColor,
borderWidth: "0.5px",
height: 40,
transitionDuration: "0.3s",
padding: "initial",
paddingRight: 6,
paddingLeft: 6,
}; };
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@@ -107,10 +114,18 @@ const styles = (theme: Theme) =>
overflow: "auto", overflow: "auto",
flexDirection: "column", flexDirection: "column",
padding: "19px 38px", padding: "19px 38px",
minHeight: "200px",
boxShadow: "none", boxShadow: "none",
border: "#EAEDEE 1px solid", border: "#EAEDEE 1px solid",
borderRadius: 3, borderRadius: 3,
minHeight: "calc(100vh - 340px)",
},
allTableSettings: {
"& .MuiTableCell-sizeSmall:last-child": {
paddingRight: "initial",
},
"& .MuiTableCell-body.MuiTableCell-sizeSmall:last-child": {
paddingRight: 6,
},
}, },
minTableHeader: { minTableHeader: {
color: "#393939", color: "#393939",
@@ -118,13 +133,15 @@ const styles = (theme: Theme) =>
"& th": { "& th": {
fontWeight: 700, fontWeight: 700,
fontSize: 14, fontSize: 14,
paddingBottom: 15, borderColor: "#39393980",
borderColor: borderColor, borderWidth: "0.5px",
padding: "6px 0 10px",
}, },
}, },
}, },
rowUnselected: { rowUnselected: {
...rowText, ...rowText,
color: "#393939",
}, },
rowSelected: { rowSelected: {
...rowText, ...rowText,
@@ -137,8 +154,12 @@ const styles = (theme: Theme) =>
padding: "5px 38px", padding: "5px 38px",
}, },
checkBoxHeader: { checkBoxHeader: {
width: 50,
textAlign: "left",
paddingRight: 10,
"&.MuiTableCell-paddingCheckbox": { "&.MuiTableCell-paddingCheckbox": {
paddingBottom: 9, paddingBottom: 4,
paddingLeft: 0,
}, },
}, },
actionsContainer: { actionsContainer: {
@@ -150,6 +171,7 @@ const styles = (theme: Theme) =>
}, },
checkBoxRow: { checkBoxRow: {
borderColor: borderColor, borderColor: borderColor,
padding: "0 10px 0 0",
}, },
loadingBox: { loadingBox: {
paddingTop: "100px", paddingTop: "100px",
@@ -160,6 +182,10 @@ const styles = (theme: Theme) =>
"&:hover": { "&:hover": {
backgroundColor: "#ececec", backgroundColor: "#ececec",
"& td": {
fontWeight: 600,
},
}, },
}, },
rowClickable: { rowClickable: {
@@ -202,7 +228,9 @@ const rowColumnsMap = (
return ( return (
<TableCell <TableCell
key={`tbRE-${column.elementKey}-${index}`} key={`tbRE-${column.elementKey}-${index}`}
className={isSelected ? classes.rowSelected : classes.rowUnselected} className={`${column.rowClass} ${
isSelected ? classes.rowSelected : classes.rowUnselected
}`}
> >
{renderElement} {renderElement}
</TableCell> </TableCell>
@@ -285,15 +313,15 @@ const TableWrapper = ({
</Grid> </Grid>
)} )}
{records && !isLoading && records.length > 0 ? ( {records && !isLoading && records.length > 0 ? (
<Table size="small" stickyHeader={stickyHeader}> <Table
size="small"
stickyHeader={stickyHeader}
className={classes.allTableSettings}
>
<TableHead className={classes.minTableHeader}> <TableHead className={classes.minTableHeader}>
<TableRow> <TableRow>
{onSelect && selectedItems && ( {onSelect && selectedItems && (
<TableCell <TableCell align="center" className={classes.checkBoxHeader}>
padding="checkbox"
align="center"
className={classes.checkBoxHeader}
>
Select Select
</TableCell> </TableCell>
)} )}
@@ -324,17 +352,13 @@ const TableWrapper = ({
key={`tb-${entityName}-${index.toString()}`} key={`tb-${entityName}-${index.toString()}`}
className={`${findView ? classes.rowClickable : ""} ${ className={`${findView ? classes.rowClickable : ""} ${
classes.rowElement classes.rowElement
}`} } rowElementRaw`}
onClick={() => { onClick={() => {
clickAction(record); clickAction(record);
}} }}
> >
{onSelect && selectedItems && ( {onSelect && selectedItems && (
<TableCell <TableCell align="center" className={classes.checkBoxRow}>
padding="checkbox"
align="center"
className={classes.checkBoxRow}
>
<Checkbox <Checkbox
value={isString(record) ? record : record[idField]} value={isString(record) ? record : record[idField]}
color="primary" color="primary"

View File

@@ -20,7 +20,6 @@ import Grid from "@material-ui/core/Grid";
import { IElementValue, KVField } from "./types"; import { IElementValue, KVField } from "./types";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary"; import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import CSVMultiSelector from "../Common/FormComponents/CSVMultiSelector/CSVMultiSelector"; import CSVMultiSelector from "../Common/FormComponents/CSVMultiSelector/CSVMultiSelector";
import CommentBoxWrapper from "../Common/FormComponents/CommentBoxWrapper/CommentBoxWrapper"; import CommentBoxWrapper from "../Common/FormComponents/CommentBoxWrapper/CommentBoxWrapper";
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
@@ -79,6 +78,7 @@ const ConfTargetGeneric = ({
}); });
setValueHolder(values); setValueHolder(values);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fields, defaultVals]); }, [fields, defaultVals]);
useEffect(() => { useEffect(() => {

View File

@@ -18,7 +18,6 @@ import React, { useState } from "react";
import get from "lodash/get"; import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { TextField } from "@material-ui/core"; import { TextField } from "@material-ui/core";
import InputAdornment from "@material-ui/core/InputAdornment"; import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search";
@@ -63,7 +62,6 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
configuration_id: "", configuration_id: "",
configuration_label: "", configuration_label: "",
}); });
const [error, setError] = useState("");
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
const tableActions = [ const tableActions = [
@@ -103,7 +101,6 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
<PageHeader label="Configurations List" /> <PageHeader label="Configurations List" />
<Grid container> <Grid container>
<Grid item xs={12} className={classes.container}> <Grid item xs={12} className={classes.container}>
{error !== "" && <Grid container>{error}</Grid>}
<Grid item xs={12} className={classes.actionsTray}> <Grid item xs={12} className={classes.actionsTray}>
<TextField <TextField
placeholder="Filter" placeholder="Filter"

View File

@@ -94,10 +94,9 @@ const panels = {
const WebhookPanel = ({ match, classes }: IWebhookPanel) => { const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
const [addWebhookOpen, setAddWebhookOpen] = useState<boolean>(false); const [addWebhookOpen, setAddWebhookOpen] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [filter, setFilter] = useState<string>(""); const [filter, setFilter] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [webhooks, setWebhooks] = useState<IWebhook[]>([]); // const [webhooks, setWebhooks] = useState<IWebhook[]>([]);
const pathIn = get(match, "path", ""); const pathIn = get(match, "path", "");
const panelToDisplay = pathIn.split("/"); const panelToDisplay = pathIn.split("/");
@@ -107,6 +106,8 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
return null; return null;
} }
const webhooks: IWebhook[] = [];
const filteredRecords: IWebhook[] = webhooks.filter((elementItem) => const filteredRecords: IWebhook[] = webhooks.filter((elementItem) =>
elementItem.name.toLocaleLowerCase().includes(filter.toLocaleLowerCase()) elementItem.name.toLocaleLowerCase().includes(filter.toLocaleLowerCase())
); );
@@ -137,7 +138,6 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
<Grid item xs={12}> <Grid item xs={12}>
<br /> <br />
</Grid> </Grid>
{error !== "" && <Grid container>{error}</Grid>}
<Grid item xs={12} className={classes.actionsTray}> <Grid item xs={12} className={classes.actionsTray}>
<TextField <TextField
placeholder="Filter" placeholder="Filter"

View File

@@ -16,7 +16,6 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { FormControlLabel, Switch } from "@material-ui/core";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector"; import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";

View File

@@ -16,7 +16,6 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { FormControlLabel, Switch } from "@material-ui/core";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector"; import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
@@ -201,6 +200,8 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
setUser(kv.get("user") ? kv.get("user") + "" : ""); setUser(kv.get("user") ? kv.get("user") + "" : "");
setPassword(kv.get("password") ? kv.get("password") + "" : ""); setPassword(kv.get("password") ? kv.get("password") + "" : "");
setSslMode(kv.get("sslmode") ? kv.get("sslmode") + "" : " "); setSslMode(kv.get("sslmode") ? kv.get("sslmode") + "" : " ");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [useConnectionString]); }, [useConnectionString]);
return ( return (

View File

@@ -16,27 +16,12 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import clsx from "clsx"; import clsx from "clsx";
import { import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
createStyles,
StyledProps,
Theme,
withStyles,
} from "@material-ui/core/styles";
import CssBaseline from "@material-ui/core/CssBaseline"; import CssBaseline from "@material-ui/core/CssBaseline";
import Drawer from "@material-ui/core/Drawer"; import Drawer from "@material-ui/core/Drawer";
import Box from "@material-ui/core/Box";
import Typography from "@material-ui/core/Typography";
import Container from "@material-ui/core/Container"; import Container from "@material-ui/core/Container";
import Link from "@material-ui/core/Link";
import history from "../../history"; import history from "../../history";
import { import { Redirect, Route, Router, Switch } from "react-router-dom";
Redirect,
Route,
RouteComponentProps,
Router,
Switch,
withRouter,
} from "react-router-dom";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { AppState } from "../../store"; import { AppState } from "../../store";
import { import {
@@ -44,7 +29,6 @@ import {
serverNeedsRestart, serverNeedsRestart,
setMenuOpen, setMenuOpen,
} from "../../actions"; } from "../../actions";
import { ThemedComponentProps } from "@material-ui/core/styles/withTheme";
import Buckets from "./Buckets/Buckets"; import Buckets from "./Buckets/Buckets";
import Policies from "./Policies/Policies"; import Policies from "./Policies/Policies";
import Permissions from "./Permissions/Permissions"; import Permissions from "./Permissions/Permissions";
@@ -58,8 +42,6 @@ import ListNotificationEndpoints from "./NotificationEndopoints/ListNotification
import ConfigurationsList from "./Configurations/ConfigurationPanels/ConfigurationsList"; import ConfigurationsList from "./Configurations/ConfigurationPanels/ConfigurationsList";
import { Button, LinearProgress } from "@material-ui/core"; import { Button, LinearProgress } from "@material-ui/core";
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel"; import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
import Trace from "./Trace/Trace";
import Logs from "./Logs/Logs";
import Heal from "./Heal/Heal"; import Heal from "./Heal/Heal";
import Watch from "./Watch/Watch"; import Watch from "./Watch/Watch";
import ListTenants from "./Tenants/ListTenants/ListTenants"; import ListTenants from "./Tenants/ListTenants/ListTenants";
@@ -147,6 +129,8 @@ const styles = (theme: Theme) =>
container: { container: {
paddingBottom: theme.spacing(4), paddingBottom: theme.spacing(4),
margin: 0, margin: 0,
width: "100%",
maxWidth: "initial",
}, },
paper: { paper: {
padding: theme.spacing(2), padding: theme.spacing(2),
@@ -248,7 +232,11 @@ const Console = ({
}, },
{ {
component: ListObjects, component: ListObjects,
path: "/object-browser/:bucket?", path: "/object-browser/:bucket",
},
{
component: ListObjects,
path: "/object-browser/:bucket/*",
}, },
{ {
component: Watch, component: Watch,
@@ -266,14 +254,6 @@ const Console = ({
component: Policies, component: Policies,
path: "/policies", path: "/policies",
}, },
{
component: Trace,
path: "/trace",
},
{
component: Logs,
path: "/logs",
},
{ {
component: Heal, component: Heal,
path: "/heal", path: "/heal",
@@ -319,7 +299,7 @@ const Console = ({
return ( return (
<React.Fragment> <React.Fragment>
{session.status == "ok" ? ( {session.status === "ok" ? (
<div className={classes.root}> <div className={classes.root}>
<CssBaseline /> <CssBaseline />
<Drawer <Drawer

View File

@@ -20,9 +20,6 @@ import clsx from "clsx";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import NetworkCheckIcon from "@material-ui/icons/NetworkCheck";
import PieChartIcon from "@material-ui/icons/PieChart";
import ViewHeadlineIcon from "@material-ui/icons/ViewHeadline";
import { Usage } from "./types"; import { Usage } from "./types";
import api from "../../../common/api"; import api from "../../../common/api";
import { niceBytes } from "../../../common/utils"; import { niceBytes } from "../../../common/utils";

View File

@@ -27,7 +27,6 @@ import api from "../../../common/api";
import UsersSelectors from "./UsersSelectors"; import UsersSelectors from "./UsersSelectors";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper"; import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
interface IGroupProps { interface IGroupProps {

View File

@@ -17,7 +17,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment"; import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search";

View File

@@ -20,7 +20,6 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { LinearProgress } from "@material-ui/core"; import { LinearProgress } from "@material-ui/core";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Title from "../../../common/Title";
import { UsersList } from "../Users/types"; import { UsersList } from "../Users/types";
import { usersSort } from "../../../utils/sortFunctions"; import { usersSort } from "../../../utils/sortFunctions";
import api from "../../../common/api"; import api from "../../../common/api";
@@ -28,10 +27,7 @@ import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment"; import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search";
import TableWrapper from "../Common/TableWrapper/TableWrapper"; import TableWrapper from "../Common/TableWrapper/TableWrapper";
import { import { actionsTray } from "../Common/FormComponents/common/styleLibrary";
actionsTray,
predefinedList,
} from "../Common/FormComponents/common/styleLibrary";
interface IGroupsProps { interface IGroupsProps {
classes: any; classes: any;

View File

@@ -1,12 +1,6 @@
import { HorizontalBar } from "react-chartjs-2"; import { HorizontalBar } from "react-chartjs-2";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import { Button, Grid, TextField, InputBase } from "@material-ui/core";
Button,
Grid,
Typography,
TextField,
Checkbox,
} from "@material-ui/core";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket"; import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { wsProtocol } from "../../../utils/wsUtils"; import { wsProtocol } from "../../../utils/wsUtils";
@@ -15,8 +9,13 @@ import { FormControl, MenuItem, Select } from "@material-ui/core";
import { BucketList, Bucket } from "../Watch/types"; import { BucketList, Bucket } from "../Watch/types";
import { HealStatus, colorH } from "./types"; import { HealStatus, colorH } from "./types";
import { niceBytes } from "../../../common/utils"; import { niceBytes } from "../../../common/utils";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary"; import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader"; import PageHeader from "../Common/PageHeader/PageHeader";
import CheckboxWrapper from "../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@@ -35,31 +34,27 @@ const styles = (theme: Theme) =>
borderBottom: "1px solid #dedede", borderBottom: "1px solid #dedede",
}, },
}, },
actionsTray: { graphContainer: {
textAlign: "right", backgroundColor: "#fff",
"& button": { border: "#EAEDEE 1px solid",
marginLeft: 10, borderRadius: 3,
}, padding: "19px 38px",
}, },
inputField: { scanInfo: {
background: "#FFFFFF", marginTop: 20,
padding: 12, display: "flex",
borderRadius: 5, flexDirection: "row",
marginLeft: 10, justifyContent: "space-between",
boxShadow: "0px 3px 6px #00000012",
}, },
fieldContainer: { scanData: {
background: "#FFFFFF", fontSize: 13,
padding: 0,
borderRadius: 5,
marginLeft: 10,
textAlign: "left",
minWidth: "206",
boxShadow: "0px 3px 6px #00000012",
}, },
lastElementWPadding: { inlineCheckboxes: {
paddingRight: "78", display: "flex",
justifyContent: "flex-start",
}, },
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)), ...containerForHeader(theme.spacing(4)),
}); });
@@ -67,6 +62,26 @@ interface IHeal {
classes: any; classes: any;
} }
const SelectStyled = withStyles((theme: Theme) =>
createStyles({
root: {
width: 450,
lineHeight: 1,
marginRight: 15,
"label + &": {
marginTop: theme.spacing(3),
},
"& .MuiSelect-select:focus": {
backgroundColor: "transparent",
},
},
input: {
fontSize: 13,
lineHeight: 15,
},
})
)(InputBase);
const Heal = ({ classes }: IHeal) => { const Heal = ({ classes }: IHeal) => {
const [start, setStart] = useState(false); const [start, setStart] = useState(false);
const [bucketName, setBucketName] = useState("Select Bucket"); const [bucketName, setBucketName] = useState("Select Bucket");
@@ -171,7 +186,7 @@ const Heal = ({ classes }: IHeal) => {
}; };
} }
} }
}, [start]); }, [start, bucketName, forceStart, forceStop, prefix, recursive]);
let data = { let data = {
labels: ["Green", "Yellow", "Red", "Grey"], labels: ["Green", "Yellow", "Red", "Grey"],
@@ -210,8 +225,8 @@ const Heal = ({ classes }: IHeal) => {
onChange={(e) => { onChange={(e) => {
setBucketName(e.target.value as string); setBucketName(e.target.value as string);
}} }}
className={classes.fieldContainer} className={classes.searchField}
disabled={false} input={<SelectStyled />}
> >
<MenuItem value="" key={`select-bucket-name-default`}> <MenuItem value="" key={`select-bucket-name-default`}>
Select Bucket Select Bucket
@@ -228,7 +243,7 @@ const Heal = ({ classes }: IHeal) => {
</FormControl> </FormControl>
<TextField <TextField
placeholder="Prefix" placeholder="Prefix"
className={classes.inputField} className={classes.searchField}
id="prefix-resource" id="prefix-resource"
label="" label=""
disabled={false} disabled={false}
@@ -248,76 +263,75 @@ const Heal = ({ classes }: IHeal) => {
> >
Start Start
</Button> </Button>
<Grid item xs={12}> </Grid>
<span>{"Recursive"}</span> <Grid item xs={12} className={classes.inlineCheckboxes}>
<Checkbox <CheckboxWrapper
name="recursive" name="recursive"
id="recursive" id="recursive"
value="recursive" value="recursive"
color="primary" checked={recursive}
inputProps={{ "aria-label": "secondary checkbox" }} onChange={(e) => {
checked={recursive} setRecursive(e.target.checked);
onChange={(e) => { }}
setRecursive(e.target.checked); disabled={false}
}} label="Recursive"
disabled={false} />
/> <CheckboxWrapper
<span>{"Force Start"}</span> name="forceStart"
<Checkbox id="forceStart"
name="recursive" value="forceStart"
id="recursive" checked={forceStart}
value="recursive" onChange={(e) => {
color="primary" setForceStart(e.target.checked);
inputProps={{ "aria-label": "secondary checkbox" }} }}
checked={forceStart} disabled={false}
onChange={(e) => { label="Force Start"
setForceStart(e.target.checked); />
}} <CheckboxWrapper
disabled={false} name="forceStop"
/> id="forceStop"
<span>{"Force Stop"}</span> value="forceStop"
<Checkbox checked={forceStop}
name="recursive" onChange={(e) => {
id="recursive" setForceStop(e.target.checked);
value="recursive" }}
color="primary" disabled={false}
inputProps={{ "aria-label": "secondary checkbox" }} label="Force Stop"
checked={forceStop} />
onChange={(e) => { </Grid>
setForceStop(e.target.checked); <Grid item xs={12}>
}} <br />
disabled={false} </Grid>
/> <Grid item xs={12} className={classes.graphContainer}>
<span className={classes.lastElementWPadding}>{""}</span> <HorizontalBar
data={data}
width={80}
height={30}
options={{
title: {
display: true,
text: "Item's Health Status [%]",
fontSize: 20,
},
legend: {
display: true,
position: "right",
},
}}
/>
<Grid item xs={12} className={classes.scanInfo}>
<div className={classes.scanData}>
<strong>Size scanned:</strong> {hStatus.sizeScanned}
</div>
<div className={classes.scanData}>
<strong>Objects healed:</strong> {hStatus.objectsHealed} /{" "}
{hStatus.objectsScanned}
</div>
<div className={classes.scanData}>
<strong>Healing time:</strong> {hStatus.healDuration}s
</div>
</Grid> </Grid>
</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> </Grid>
</Grid> </Grid>
</React.Fragment> </React.Fragment>

View File

@@ -19,16 +19,11 @@ import { connect } from "react-redux";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import ListItem from "@material-ui/core/ListItem"; import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemIcon from "@material-ui/core/ListItemIcon";
import WebAssetIcon from "@material-ui/icons/WebAsset";
import HealingIcon from "@material-ui/icons/Healing";
import DescriptionIcon from "@material-ui/icons/Description"; import DescriptionIcon from "@material-ui/icons/Description";
import FileCopyIcon from "@material-ui/icons/FileCopy";
import Collapse from "@material-ui/core/Collapse"; import Collapse from "@material-ui/core/Collapse";
import ListItemText from "@material-ui/core/ListItemText"; import ListItemText from "@material-ui/core/ListItemText";
import List from "@material-ui/core/List"; import List from "@material-ui/core/List";
import { Divider, Typography, withStyles } from "@material-ui/core"; import { Divider, 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 { createStyles, Theme } from "@material-ui/core/styles";
import history from "../../../history"; import history from "../../../history";
import logo from "../../../icons/minio_console_logo.svg"; import logo from "../../../icons/minio_console_logo.svg";
@@ -48,13 +43,11 @@ import {
LambdaNotificationsIcon, LambdaNotificationsIcon,
MirroringIcon, MirroringIcon,
ServiceAccountsIcon, ServiceAccountsIcon,
TraceIcon,
UsersIcon, UsersIcon,
WarpIcon, WarpIcon,
} from "../../../icons"; } from "../../../icons";
import { clearSession } from "../../../common/utils"; import { clearSession } from "../../../common/utils";
import HealIcon from "../../../icons/HealIcon"; import HealIcon from "../../../icons/HealIcon";
import ConsoleIcon from "../../../icons/ConsoleIcon";
import LicenseIcon from "../../../icons/LicenseIcon"; import LicenseIcon from "../../../icons/LicenseIcon";
import LogoutIcon from "../../../icons/LogoutIcon"; import LogoutIcon from "../../../icons/LogoutIcon";
@@ -247,14 +240,6 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
name: "IAM Policies", name: "IAM Policies",
icon: <IAMPoliciesIcon />, icon: <IAMPoliciesIcon />,
}, },
{
group: "Tools",
type: "item",
component: NavLink,
to: "/logs",
name: "Logs",
icon: <ConsoleIcon />,
},
{ {
group: "Tools", group: "Tools",
type: "item", type: "item",
@@ -263,14 +248,6 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
name: "Watch", name: "Watch",
icon: <WatchIcon />, icon: <WatchIcon />,
}, },
{
group: "Tools",
type: "item",
component: NavLink,
to: "/trace",
name: "Trace",
icon: <TraceIcon />,
},
{ {
group: "Tools", group: "Tools",
type: "item", type: "item",
@@ -362,7 +339,7 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
(menuItem: any) => menuItem.type !== "title" (menuItem: any) => menuItem.type !== "title"
); );
if (countableElements.length == 0) { if (countableElements.length === 0) {
return null; return null;
} }
@@ -429,6 +406,7 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
); );
} }
default: default:
return null;
} }
})} })}
<Divider /> <Divider />

View File

@@ -62,6 +62,7 @@ const styles = (theme: Theme) =>
height: "80px", height: "80px",
}, },
lambdaNotif: { lambdaNotif: {
backgroundColor: "#fff",
border: "#393939 1px solid", border: "#393939 1px solid",
borderRadius: 5, borderRadius: 5,
width: 101, width: 101,
@@ -289,7 +290,7 @@ const AddNotificationEndpoint = ({
<div className={classes.iconContainer}> <div className={classes.iconContainer}>
{withLogos.map((item) => { {withLogos.map((item) => {
return ( return (
<a <button
key={`icon-${item.targetTitle}`} key={`icon-${item.targetTitle}`}
className={classes.lambdaNotif} className={classes.lambdaNotif}
onClick={() => { onClick={() => {
@@ -301,7 +302,7 @@ const AddNotificationEndpoint = ({
className={classes.logoButton} className={classes.logoButton}
alt={item.targetTitle} alt={item.targetTitle}
/> />
</a> </button>
); );
})} })}
</div> </div>

View File

@@ -20,7 +20,6 @@ import { TextField } from "@material-ui/core";
import { red } from "@material-ui/core/colors"; import { red } from "@material-ui/core/colors";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Typography from "@material-ui/core/Typography";
import InputAdornment from "@material-ui/core/InputAdornment"; import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions"; import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";

View File

@@ -15,11 +15,11 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import get from "lodash/get"; import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import AddBucket from "../Buckets/ListBuckets/AddBucket";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment"; import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search";
@@ -28,13 +28,16 @@ import { CreateIcon } from "../../../icons";
import { niceBytes } from "../../../common/utils"; import { niceBytes } from "../../../common/utils";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions"; import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import { Bucket, BucketList } from "../Buckets/types"; import { Bucket, BucketList } from "../Buckets/types";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import api from "../../../common/api";
import history from "../../../history";
import { import {
actionsTray, actionsTray,
objectBrowserCommon,
searchField, searchField,
} from "../Common/FormComponents/common/styleLibrary"; } from "../Common/FormComponents/common/styleLibrary";
import { addRoute, resetRoutesList } from "./actions";
import BrowserBreadcrumbs from "./BrowserBreadcrumbs";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import AddBucket from "../Buckets/ListBuckets/AddBucket";
import api from "../../../common/api";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@@ -68,20 +71,47 @@ const styles = (theme: Theme) =>
}, },
usedSpaceCol: { usedSpaceCol: {
width: 150, width: 150,
textAlign: "right",
}, },
subTitleLabel: { subTitleLabel: {
alignItems: "center", alignItems: "center",
display: "flex", display: "flex",
}, },
bucketName: {
display: "flex",
alignItems: "center",
},
iconBucket: {
backgroundImage: "url(/images/ob_bucket_clear.svg)",
backgroundRepeat: "no-repeat",
backgroundPosition: "center center",
width: 16,
height: 40,
marginRight: 10,
},
"@global": {
".rowElementRaw:hover .iconBucketElm": {
backgroundImage: "url(/images/ob_bucket_filled.svg)",
},
},
...actionsTray, ...actionsTray,
...searchField, ...searchField,
...objectBrowserCommon,
}); });
interface IBrowseBucketsProps { interface IBrowseBucketsProps {
classes: any; classes: any;
addRoute: (path: string, label: string) => any;
resetRoutesList: (doVar: boolean) => any;
match: any;
} }
const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => { const BrowseBuckets = ({
classes,
match,
addRoute,
resetRoutesList,
}: IBrowseBucketsProps) => {
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [page, setPage] = useState<number>(0); const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10); const [rowsPerPage, setRowsPerPage] = useState<number>(10);
@@ -92,6 +122,10 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
const offset = page * rowsPerPage; const offset = page * rowsPerPage;
useEffect(() => {
resetRoutesList(true);
}, [match]);
useEffect(() => { useEffect(() => {
if (loading) { if (loading) {
api api
@@ -119,21 +153,24 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
setError(err); setError(err);
}); });
} }
}, [loading]); }, [loading, offset, rowsPerPage, page]);
const closeAddModalAndRefresh = () => { const closeAddModalAndRefresh = (refresh: boolean) => {
setAddScreenOpen(false); setAddScreenOpen(false);
setLoading(false);
if (refresh) {
setLoading(true);
}
}; };
const filteredRecords = records const filteredRecords = records.filter((b: Bucket) => {
.filter((b: Bucket) => { if (filterBuckets === "") {
if (filterBuckets === "") { return true;
return true; }
} return b.name.indexOf(filterBuckets) >= 0;
return b.name.indexOf(filterBuckets) >= 0; });
})
.slice(offset, offset + rowsPerPage); const showInPage = filteredRecords.slice(offset, offset + rowsPerPage);
const handleChangePage = (event: unknown, newPage: number) => { const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage); setPage(newPage);
@@ -147,6 +184,22 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
setRowsPerPage(rPP); setRowsPerPage(rPP);
}; };
const handleViewChange = (idElement: string) => {
const currentPath = get(match, "url", "/object-browser");
const newPath = `${currentPath}/${idElement}`;
addRoute(newPath, idElement);
};
const renderBucket = (bucketName: string) => {
return (
<div className={classes.bucketName}>
<div className={`${classes.iconBucket} iconBucketElm`} />
<span>{bucketName}</span>
</div>
);
};
return ( return (
<React.Fragment> <React.Fragment>
{addScreenOpen && ( {addScreenOpen && (
@@ -156,10 +209,24 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
/> />
)} )}
<Grid container> <Grid container>
<Grid item xs={2} className={classes.subTitleLabel}> <Grid item xs={12} className={classes.obTitleSection}>
<Typography variant="h6">Buckets</Typography> <div>
<BrowserBreadcrumbs />
</div>
<div>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Create Bucket
</Button>
</div>
</Grid> </Grid>
<Grid item xs={10} className={classes.actionsTray}> <Grid item xs={12} className={classes.actionsTray}>
<TextField <TextField
placeholder="Search Buckets" placeholder="Search Buckets"
className={classes.searchField} className={classes.searchField}
@@ -177,16 +244,6 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
), ),
}} }}
/> />
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Add Bucket
</Button>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<br /> <br />
@@ -195,19 +252,28 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
{error !== "" && <span className={classes.errorBlock}>{error}</span>} {error !== "" && <span className={classes.errorBlock}>{error}</span>}
<TableWrapper <TableWrapper
itemActions={[ itemActions={[
{ type: "view", to: `/object-browser`, sendOnlyId: true }, {
type: "view",
sendOnlyId: true,
onClick: handleViewChange,
},
]} ]}
columns={[ columns={[
{ label: "Name", elementKey: "name" }, {
label: "Name",
elementKey: "name",
renderFunction: renderBucket,
},
{ {
label: "Used Space", label: "Used Space",
elementKey: "size", elementKey: "size",
renderFunction: niceBytes, renderFunction: niceBytes,
globalClass: classes.usedSpaceCol, globalClass: classes.usedSpaceCol,
rowClass: classes.usedSpaceCol,
}, },
]} ]}
isLoading={loading} isLoading={loading}
records={filteredRecords} records={showInPage}
entityName="Buckets" entityName="Buckets"
idField="name" idField="name"
paginatorConfig={{ paginatorConfig={{
@@ -231,4 +297,11 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
); );
}; };
export default withStyles(styles)(BrowseBuckets); const mapDispatchToProps = {
addRoute,
resetRoutesList,
};
const connector = connect(null, mapDispatchToProps);
export default withRouter(connector(withStyles(styles)(BrowseBuckets)));

View File

@@ -0,0 +1,90 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import get from "lodash/get";
import Grid from "@material-ui/core/Grid";
import { connect } from "react-redux";
import { withStyles } from "@material-ui/core";
import { createStyles, Theme } from "@material-ui/core/styles";
import { removeRouteLevel } from "./actions";
import { ObjectBrowserState, Route } from "./reducers";
import { objectBrowserCommon } from "../Common/FormComponents/common/styleLibrary";
import { Link } from "react-router-dom";
interface ObjectBrowserReducer {
objectBrowser: ObjectBrowserState;
}
interface IObjectBrowser {
classes: any;
objectsList: Route[];
removeRouteLevel: (path: string) => any;
}
const styles = (theme: Theme) =>
createStyles({
...objectBrowserCommon,
});
const BrowserBreadcrumbs = ({
classes,
objectsList,
removeRouteLevel,
}: IObjectBrowser) => {
const listBreadcrumbs = objectsList.map((objectItem, index) => {
return (
<React.Fragment key={`breadcrumbs-${index.toString()}`}>
<Link
to={objectItem.route}
onClick={() => {
removeRouteLevel(objectItem.route);
}}
>
{objectItem.label}
</Link>
{index < objectsList.length - 1 && <span> / </span>}
</React.Fragment>
);
});
return (
<React.Fragment>
<Grid item xs={12}>
<div className={classes.sectionTitle}>
{objectsList && objectsList.length > 0
? objectsList.slice(-1)[0].label
: ""}
</div>
</Grid>
<Grid item xs={12} className={classes.breadcrumbs}>
{listBreadcrumbs}
</Grid>
</React.Fragment>
);
};
const mapStateToProps = ({ objectBrowser }: ObjectBrowserReducer) => ({
objectsList: get(objectBrowser, "routesList", []),
});
const mapDispatchToProps = {
removeRouteLevel,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default connector(withStyles(styles)(BrowserBreadcrumbs));

View File

@@ -17,7 +17,7 @@
import React from "react"; import React from "react";
import get from "lodash/get"; import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Grid, Typography } from "@material-ui/core"; import { Grid } from "@material-ui/core";
import BrowseBuckets from "./BrowseBuckets"; import BrowseBuckets from "./BrowseBuckets";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary"; import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader"; import PageHeader from "../Common/PageHeader/PageHeader";
@@ -73,7 +73,7 @@ const styles = (theme: Theme) =>
}); });
const ObjectBrowser = ({ match, classes }: IObjectBrowserProps) => { const ObjectBrowser = ({ match, classes }: IObjectBrowserProps) => {
const pathIn = get(match, "path", ""); const pathIn = get(match, "url", "");
return ( return (
<React.Fragment> <React.Fragment>

View File

@@ -0,0 +1,93 @@
// 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 OBJECT_BROWSER_ADD_ROUTE = "OBJECT_BROWSER/ADD_ROUTE";
export const OBJECT_BROWSER_RESET_ROUTES_LIST =
"OBJECT_BROWSER/RESET_ROUTES_LIST";
export const OBJECT_BROWSER_REMOVE_ROUTE_LEVEL =
"OBJECT_BROWSER/REMOVE_ROUTE_LEVEL";
export const OBJECT_BROWSER_SET_ALL_ROUTES = "OBJECT_BROWSER/SET_ALL_ROUTES";
export const OBJECT_BROWSER_CREATE_FOLDER = "OBJECT_BROWSER/CREATE_FOLDER";
interface AddRouteAction {
type: typeof OBJECT_BROWSER_ADD_ROUTE;
route: string;
label: string;
}
interface ResetRoutesList {
type: typeof OBJECT_BROWSER_RESET_ROUTES_LIST;
reset: boolean;
}
interface RemoveRouteLevel {
type: typeof OBJECT_BROWSER_REMOVE_ROUTE_LEVEL;
toRoute: string;
}
interface SetAllRoutes {
type: typeof OBJECT_BROWSER_SET_ALL_ROUTES;
currentRoute: string;
}
interface CreateFolder {
type: typeof OBJECT_BROWSER_CREATE_FOLDER;
newRoute: string;
}
export type ObjectBrowserActionTypes =
| AddRouteAction
| ResetRoutesList
| RemoveRouteLevel
| SetAllRoutes
| CreateFolder;
export const addRoute = (route: string, label: string) => {
return {
type: OBJECT_BROWSER_ADD_ROUTE,
route,
label,
};
};
export const resetRoutesList = (reset: boolean) => {
console.log("RESET");
return {
type: OBJECT_BROWSER_RESET_ROUTES_LIST,
reset,
};
};
export const removeRouteLevel = (toRoute: string) => {
return {
type: OBJECT_BROWSER_REMOVE_ROUTE_LEVEL,
toRoute,
};
};
export const setAllRoutes = (currentRoute: string) => {
return {
type: OBJECT_BROWSER_SET_ALL_ROUTES,
currentRoute,
};
};
export const createFolder = (newRoute: string) => {
return {
type: OBJECT_BROWSER_CREATE_FOLDER,
newRoute,
};
};

View File

@@ -0,0 +1,114 @@
// 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 history from "../../../history";
import {
OBJECT_BROWSER_ADD_ROUTE,
OBJECT_BROWSER_CREATE_FOLDER,
OBJECT_BROWSER_REMOVE_ROUTE_LEVEL,
OBJECT_BROWSER_RESET_ROUTES_LIST,
OBJECT_BROWSER_SET_ALL_ROUTES,
ObjectBrowserActionTypes,
} from "./actions";
export interface Route {
route: string;
label: string;
}
export interface ObjectBrowserState {
routesList: Route[];
}
const initialRoute = [{ route: "/object-browser", label: "All Buckets" }];
const initialState: ObjectBrowserState = {
routesList: initialRoute,
};
export function objectBrowserReducer(
state = initialState,
action: ObjectBrowserActionTypes
): ObjectBrowserState {
switch (action.type) {
case OBJECT_BROWSER_ADD_ROUTE:
const newRouteList = [
...state.routesList,
{ route: action.route, label: action.label },
];
history.push(action.route);
return { ...state, routesList: newRouteList };
case OBJECT_BROWSER_RESET_ROUTES_LIST:
return {
...state,
routesList: [...initialRoute],
};
case OBJECT_BROWSER_REMOVE_ROUTE_LEVEL:
const indexOfTopPath =
state.routesList.findIndex(
(element) => element.route === action.toRoute
) + 1;
const newRouteLevels = state.routesList.slice(0, indexOfTopPath);
return {
...state,
routesList: newRouteLevels,
};
case OBJECT_BROWSER_SET_ALL_ROUTES:
const splitRoutes = action.currentRoute.split("/");
const routesArray: Route[] = [];
let initRoute = initialRoute[0].route;
splitRoutes.forEach((route) => {
if (route !== "" && route !== "object-browser") {
initRoute = `${initRoute}/${route}`;
routesArray.push({ route: initRoute, label: route });
}
});
const newSetOfRoutes = [...initialRoute, ...routesArray];
return {
...state,
routesList: newSetOfRoutes,
};
case OBJECT_BROWSER_CREATE_FOLDER:
const newFoldersRoutes = [...state.routesList];
let lastRoute = state.routesList[state.routesList.length - 1].route;
const splitElements = action.newRoute.split("/");
splitElements.forEach((element) => {
const folderTrim = element.trim();
if (folderTrim !== "") {
lastRoute = `${lastRoute}/${folderTrim}`;
const newItem = { route: lastRoute, label: folderTrim };
newFoldersRoutes.push(newItem);
}
});
history.push(lastRoute);
return {
...state,
routesList: newFoldersRoutes,
};
default:
return state;
}
}

View File

@@ -19,7 +19,6 @@ import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button } from "@material-ui/core"; import { Button } from "@material-ui/core";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment"; import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search";
@@ -78,7 +77,6 @@ interface IPoliciesProps {
const Policies = ({ classes }: IPoliciesProps) => { const Policies = ({ classes }: IPoliciesProps) => {
const [records, setRecords] = useState<Policy[]>([]); const [records, setRecords] = useState<Policy[]>([]);
const [totalRecords, setTotalRecords] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false); const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
@@ -100,7 +98,6 @@ const Policies = ({ classes }: IPoliciesProps) => {
.invoke("GET", `/api/v1/policies?offset=${offset}&limit=${rowsPerPage}`) .invoke("GET", `/api/v1/policies?offset=${offset}&limit=${rowsPerPage}`)
.then((res: PolicyList) => { .then((res: PolicyList) => {
const policies = get(res, "policies", []); const policies = get(res, "policies", []);
const total = get(res, "total", 0);
policies.sort((pa, pb) => { policies.sort((pa, pb) => {
if (pa.name > pb.name) { if (pa.name > pb.name) {
@@ -116,7 +113,6 @@ const Policies = ({ classes }: IPoliciesProps) => {
setLoading(false); setLoading(false);
setRecords(policies); setRecords(policies);
setTotalRecords(total);
setError(""); setError("");
// if we get 0 results, and page > 0 , go down 1 page // if we get 0 results, and page > 0 , go down 1 page
@@ -137,15 +133,7 @@ const Policies = ({ classes }: IPoliciesProps) => {
setError(err); setError(err);
}); });
} }
}, [ }, [loading, setLoading, setRecords, setError, setPage, page, rowsPerPage]);
loading,
setLoading,
setRecords,
setTotalRecords,
setError,
setPage,
setError,
]);
const fetchRecords = () => { const fetchRecords = () => {
setLoading(true); setLoading(true);
@@ -256,6 +244,11 @@ const Policies = ({ classes }: IPoliciesProps) => {
<Grid item xs={12}> <Grid item xs={12}>
<br /> <br />
</Grid> </Grid>
{error && (
<Grid item xs={12}>
{error}
</Grid>
)}
<Grid item xs={12}> <Grid item xs={12}>
<TableWrapper <TableWrapper
itemActions={tableActions} itemActions={tableActions}

View File

@@ -14,20 +14,10 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useCallback, useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import get from "lodash/get"; import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { import { Button, LinearProgress } from "@material-ui/core";
Button,
LinearProgress,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@material-ui/core";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import { import {
modalBasic, modalBasic,
@@ -35,10 +25,7 @@ import {
} from "../Common/FormComponents/common/styleLibrary"; } from "../Common/FormComponents/common/styleLibrary";
import { User } from "../Users/types"; import { User } from "../Users/types";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper"; import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import { Policy, PolicyList } from "./types";
import api from "../../../common/api"; import api from "../../../common/api";
import { policySort } from "../../../utils/sortFunctions";
import { Group } from "../Groups/types";
import PolicySelectors from "./PolicySelectors"; import PolicySelectors from "./PolicySelectors";
interface ISetPolicyProps { interface ISetPolicyProps {
@@ -132,7 +119,8 @@ const SetPolicy = ({
setActualPolicy(userPolicy); setActualPolicy(userPolicy);
setSelectedPolicy(userPolicy); setSelectedPolicy(userPolicy);
} }
}, [open]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, selectedGroup, selectedUser]);
const userName = get(selectedUser, "accessKey", ""); const userName = get(selectedUser, "accessKey", "");
@@ -144,6 +132,7 @@ const SetPolicy = ({
modalOpen={open} modalOpen={open}
title="Set Policies" title="Set Policies"
> >
{error !== "" && <span className={classes.error}>{error}</span>}
<Grid item xs={12}> <Grid item xs={12}>
<Grid item xs={12} className={classes.predefinedTitle}> <Grid item xs={12} className={classes.predefinedTitle}>
Selected {selectedGroup !== null ? "Group" : "User"} Selected {selectedGroup !== null ? "Group" : "User"}

View File

@@ -17,7 +17,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import { Button, LinearProgress, Tooltip } from "@material-ui/core"; import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary"; import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper"; import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";

View File

@@ -100,6 +100,13 @@ const styles = (theme: Theme) =>
color: "#dc1f2e", color: "#dc1f2e",
fontSize: "0.75rem", fontSize: "0.75rem",
}, },
h3Section: {
marginTop: 0,
},
descriptionText: {
fontSize: 13,
color: "#777777",
},
...modalBasic, ...modalBasic,
}); });
@@ -265,6 +272,8 @@ const AddTenant = ({
/*Calculate Allocation*/ /*Calculate Allocation*/
useEffect(() => { useEffect(() => {
validateClusterSize(); validateClusterSize();
setECParityChoices([]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodes, volumeSize, sizeFactor]); }, [nodes, volumeSize, sizeFactor]);
const validateClusterSize = () => { const validateClusterSize = () => {
@@ -660,6 +669,7 @@ const AddTenant = ({
setEncryptionValid(Object.keys(commonVal).length === 0); setEncryptionValid(Object.keys(commonVal).length === 0);
setValidationErrors(commonVal); setValidationErrors(commonVal);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
enableEncryption, enableEncryption,
encryptionType, encryptionType,
@@ -744,7 +754,7 @@ const AddTenant = ({
}, },
resources: { resources: {
requests: { requests: {
memory: parseInt(getBytes(memoryNode, "Gi")), memory: parseInt(getBytes(memoryNode, "GiB")),
}, },
}, },
affinity: hardCodedAffinity, affinity: hardCodedAffinity,
@@ -920,6 +930,7 @@ const AddTenant = ({
setAddError(err); setAddError(err);
}); });
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [addSending]); }, [addSending]);
const storeCertInObject = (certName: string, certValue: string) => { const storeCertInObject = (certName: string, certValue: string) => {
@@ -945,8 +956,10 @@ const AddTenant = ({
componentRender: ( componentRender: (
<React.Fragment> <React.Fragment>
<div className={classes.headerElement}> <div className={classes.headerElement}>
<h3>Name Tenant</h3> <h3 className={classes.h3Section}>Name Tenant</h3>
<span>How would you like to name this new tenant?</span> <span className={classes.descriptionText}>
How would you like to name this new tenant?
</span>
</div> </div>
<Grid item xs={12}> <Grid item xs={12}>
<InputBoxWrapper <InputBoxWrapper
@@ -981,7 +994,6 @@ const AddTenant = ({
id="storage_class" id="storage_class"
name="storage_class" name="storage_class"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => { onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
console.log(e.target.value as string);
setSelectedStorageClass(e.target.value as string); setSelectedStorageClass(e.target.value as string);
}} }}
label="Storage Class" label="Storage Class"
@@ -992,7 +1004,7 @@ const AddTenant = ({
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<br /> <br />
<span> <span className={classes.descriptionText}>
Check 'Advanced Mode' for additional configuration options, such Check 'Advanced Mode' for additional configuration options, such
as IDP, Disk Encryption, and customized TLS/SSL Certificates. as IDP, Disk Encryption, and customized TLS/SSL Certificates.
<br /> <br />
@@ -1028,8 +1040,10 @@ const AddTenant = ({
componentRender: ( componentRender: (
<React.Fragment> <React.Fragment>
<div className={classes.headerElement}> <div className={classes.headerElement}>
<h3>Configure</h3> <h3 className={classes.h3Section}>Configure</h3>
<span>Basic configurations for tenant management</span> <span className={classes.descriptionText}>
Basic configurations for tenant management
</span>
</div> </div>
<Grid item xs={12}> <Grid item xs={12}>
@@ -1111,8 +1125,8 @@ const AddTenant = ({
componentRender: ( componentRender: (
<React.Fragment> <React.Fragment>
<div className={classes.headerElement}> <div className={classes.headerElement}>
<h3>IDP</h3> <h3 className={classes.h3Section}>IDP</h3>
<span> <span className={classes.descriptionText}>
Access to the tenant can be controlled via an external Identity Access to the tenant can be controlled via an external Identity
Manager. Manager.
</span> </span>
@@ -1299,7 +1313,7 @@ const AddTenant = ({
componentRender: ( componentRender: (
<React.Fragment> <React.Fragment>
<div className={classes.headerElement}> <div className={classes.headerElement}>
<h3>Security</h3> <h3 className={classes.h3Section}>Security</h3>
</div> </div>
<Grid item xs={12}> <Grid item xs={12}>
<FormSwitchWrapper <FormSwitchWrapper
@@ -1421,8 +1435,10 @@ const AddTenant = ({
componentRender: ( componentRender: (
<React.Fragment> <React.Fragment>
<div className={classes.headerElement}> <div className={classes.headerElement}>
<h3>Encryption</h3> <h3 className={classes.h3Section}>Encryption</h3>
<span>How would you like to encrypt the information at rest.</span> <span className={classes.descriptionText}>
How would you like to encrypt the information at rest.
</span>
</div> </div>
<Grid item xs={12}> <Grid item xs={12}>
<FormSwitchWrapper <FormSwitchWrapper
@@ -1883,8 +1899,10 @@ const AddTenant = ({
componentRender: ( componentRender: (
<React.Fragment> <React.Fragment>
<div className={classes.headerElement}> <div className={classes.headerElement}>
<h3>Tenant Size</h3> <h3 className={classes.h3Section}>Tenant Size</h3>
<span>Please select the desired capacity</span> <span className={classes.descriptionText}>
Please select the desired capacity
</span>
</div> </div>
<span className={classes.error}>{distribution.error}</span> <span className={classes.error}>{distribution.error}</span>
<Grid item xs={12}> <Grid item xs={12}>
@@ -1923,7 +1941,7 @@ const AddTenant = ({
</div> </div>
<div className={classes.sizeFactorContainer}> <div className={classes.sizeFactorContainer}>
<SelectWrapper <SelectWrapper
label="" label={"Unit"}
id="size_factor" id="size_factor"
name="size_factor" name="size_factor"
value={sizeFactor} value={sizeFactor}
@@ -1963,7 +1981,7 @@ const AddTenant = ({
value={ecParity} value={ecParity}
options={ecParityChoices} options={ecParityChoices}
/> />
<span> <span className={classes.descriptionText}>
Please select the desired parity. This setting will change the Please select the desired parity. This setting will change the
max usable capacity in the cluster max usable capacity in the cluster
</span> </span>
@@ -2011,8 +2029,10 @@ const AddTenant = ({
componentRender: ( componentRender: (
<React.Fragment> <React.Fragment>
<div className={classes.headerElement}> <div className={classes.headerElement}>
<h3>Review</h3> <h3 className={classes.h3Section}>Review</h3>
<span>Review the details of the new tenant</span> <span className={classes.descriptionText}>
Review the details of the new tenant
</span>
</div> </div>
{addError !== "" && ( {addError !== "" && (
<Grid item xs={12}> <Grid item xs={12}>

View File

@@ -69,6 +69,7 @@ const DeleteTenant = ({
setDeleteError(err); setDeleteError(err);
}); });
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [deleteLoading]); }, [deleteLoading]);
const removeRecord = () => { const removeRecord = () => {

View File

@@ -16,7 +16,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment"; import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search";
@@ -34,7 +33,11 @@ import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
import CredentialsPrompt from "../../Common/CredentialsPrompt/CredentialsPrompt"; import CredentialsPrompt from "../../Common/CredentialsPrompt/CredentialsPrompt";
import history from "../../../../history"; import history from "../../../../history";
import RefreshIcon from "@material-ui/icons/Refresh"; import RefreshIcon from "@material-ui/icons/Refresh";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary"; import {
actionsTray,
containerForHeader,
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../Common/PageHeader/PageHeader"; import PageHeader from "../../Common/PageHeader/PageHeader";
interface ITenantsList { interface ITenantsList {
@@ -71,18 +74,8 @@ const styles = (theme: Theme) =>
}, },
}, },
}, },
actionsTray: { ...actionsTray,
textAlign: "right", ...searchField,
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...containerForHeader(theme.spacing(4)), ...containerForHeader(theme.spacing(4)),
}); });
@@ -93,7 +86,6 @@ const ListTenants = ({ classes }: ITenantsList) => {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [filterTenants, setFilterTenants] = useState<string>(""); const [filterTenants, setFilterTenants] = useState<string>("");
const [records, setRecords] = useState<any[]>([]); const [records, setRecords] = useState<any[]>([]);
const [offset, setOffset] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10); const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [page, setPage] = useState<number>(0); const [page, setPage] = useState<number>(0);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
@@ -154,17 +146,15 @@ const ListTenants = ({ classes }: ITenantsList) => {
setRowsPerPage(rPP); setRowsPerPage(rPP);
}; };
const openLink = (link: string) => {
window.open(link, "_blank");
};
const tableActions = [ const tableActions = [
{ type: "view", onClick: redirectToTenantDetails }, { type: "view", onClick: redirectToTenantDetails },
{ type: "delete", onClick: confirmDeleteTenant }, { type: "delete", onClick: confirmDeleteTenant },
]; ];
const globalOffset = 0;
const filteredRecords = records const filteredRecords = records
.slice(offset, offset + rowsPerPage) .slice(globalOffset, globalOffset + rowsPerPage)
.filter((b: any) => { .filter((b: any) => {
if (filterTenants === "") { if (filterTenants === "") {
return true; return true;
@@ -252,17 +242,6 @@ const ListTenants = ({ classes }: ITenantsList) => {
<Grid container> <Grid container>
<Grid item xs={12} className={classes.container}> <Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}> <Grid item xs={12} className={classes.actionsTray}>
<IconButton
color="primary"
aria-label="Refresh Tenant List"
component="span"
onClick={() => {
setIsLoading(true);
}}
>
<RefreshIcon />
</IconButton>
<TextField <TextField
placeholder="Search Tenants" placeholder="Search Tenants"
className={classes.searchField} className={classes.searchField}
@@ -280,6 +259,16 @@ const ListTenants = ({ classes }: ITenantsList) => {
), ),
}} }}
/> />
<IconButton
color="primary"
aria-label="Refresh Tenant List"
component="span"
onClick={() => {
setIsLoading(true);
}}
>
<RefreshIcon />
</IconButton>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
@@ -291,6 +280,11 @@ const ListTenants = ({ classes }: ITenantsList) => {
Create Tenant Create Tenant
</Button> </Button>
</Grid> </Grid>
{error !== "" && (
<Grid item xs={12}>
{error}
</Grid>
)}
<Grid item xs={12}> <Grid item xs={12}>
<br /> <br />
</Grid> </Grid>

View File

@@ -3,13 +3,8 @@ import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary"; import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import { import { niceBytes } from "../../../../common/utils";
factorForDropdown,
getTotalSize,
niceBytes,
} from "../../../../common/utils";
import { Button, LinearProgress } from "@material-ui/core"; import { Button, LinearProgress } from "@material-ui/core";
import api from "../../../../common/api"; import api from "../../../../common/api";
import { IAddZoneRequest, ITenant } from "../ListTenants/types"; import { IAddZoneRequest, ITenant } from "../ListTenants/types";

View File

@@ -15,24 +15,22 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket"; import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { AppState } from "../../../store"; import { AppState } from "../../../../../store";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { logMessageReceived, logResetMessages } from "./actions"; import { logMessageReceived, logResetMessages } from "./actions";
import { LogMessage } from "./types"; import { LogMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { timeFromDate } from "../../../common/utils"; import { timeFromDate } from "../../../../../common/utils";
import { wsProtocol } from "../../../utils/wsUtils"; import { wsProtocol } from "../../../../../utils/wsUtils";
import { import {
actionsTray, actionsTray,
containerForHeader, containerForHeader,
searchField, searchField,
} from "../Common/FormComponents/common/styleLibrary"; } from "../../../Common/FormComponents/common/styleLibrary";
import { Button, Grid } from "@material-ui/core"; import { Grid } from "@material-ui/core";
import PageHeader from "../Common/PageHeader/PageHeader";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment"; import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search";
import { CreateIcon } from "../../../icons";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@@ -74,6 +72,8 @@ interface ILogs {
logMessageReceived: typeof logMessageReceived; logMessageReceived: typeof logMessageReceived;
logResetMessages: typeof logResetMessages; logResetMessages: typeof logResetMessages;
messages: LogMessage[]; messages: LogMessage[];
namespace: string;
tenant: string;
} }
const Logs = ({ const Logs = ({
@@ -81,6 +81,8 @@ const Logs = ({
logMessageReceived, logMessageReceived,
logResetMessages, logResetMessages,
messages, messages,
namespace,
tenant,
}: ILogs) => { }: ILogs) => {
const [highlight, setHighlight] = useState(""); const [highlight, setHighlight] = useState("");
@@ -93,7 +95,7 @@ const Logs = ({
const wsProt = wsProtocol(url.protocol); const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket( const c = new W3CWebSocket(
`${wsProt}://${url.hostname}:${port}/ws/console` `${wsProt}://${url.hostname}:${port}/ws/console/${namespace}/${tenant}`
); );
let interval: any | null = null; let interval: any | null = null;
@@ -122,20 +124,7 @@ const Logs = ({
console.log("closing websockets"); console.log("closing websockets");
}; };
} }
}, [logMessageReceived]); }, [logMessageReceived, logResetMessages]);
// replaces a character of a string with other at a given index
const replaceWeirdChar = (
origString: string,
replaceChar: string,
index: number
) => {
let firstPart = origString.substr(0, index);
let lastPart = origString.substr(index + 1);
let newString = firstPart + replaceChar + lastPart;
return newString;
};
const renderError = (logElement: LogMessage) => { const renderError = (logElement: LogMessage) => {
let errorElems = []; let errorElems = [];
@@ -335,38 +324,33 @@ const Logs = ({
}); });
return ( return (
<React.Fragment> <Grid item xs={12}>
<PageHeader label="Logs" /> <Grid item xs={12} className={classes.actionsTray}>
<Grid container> <TextField
<Grid item xs={12} className={classes.container}> placeholder="Highlight Line"
<Grid item xs={12} className={classes.actionsTray}> className={classes.searchField}
<TextField id="search-resource"
placeholder="Highlight Line" label=""
className={classes.searchField} onChange={(val) => {
id="search-resource" setHighlight(val.target.value);
label="" }}
onChange={(val) => { InputProps={{
setHighlight(val.target.value); disableUnderline: true,
}} startAdornment: (
InputProps={{ <InputAdornment position="start">
disableUnderline: true, <SearchIcon />
startAdornment: ( </InputAdornment>
<InputAdornment position="start"> ),
<SearchIcon /> }}
</InputAdornment> />
),
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<div className={classes.logList}>{renderLines}</div>
</Grid>
</Grid>
</Grid> </Grid>
</React.Fragment> <Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<div className={classes.logList}>{renderLines}</div>
</Grid>
</Grid>
); );
}; };

View File

@@ -31,8 +31,9 @@ import AddZoneModal from "./AddZoneModal";
import AddBucket from "../../Buckets/ListBuckets/AddBucket"; import AddBucket from "../../Buckets/ListBuckets/AddBucket";
import ReplicationSetup from "./ReplicationSetup"; import ReplicationSetup from "./ReplicationSetup";
import api from "../../../../common/api"; import api from "../../../../common/api";
import { BucketInfo } from "../../Buckets/types";
import { ITenant, IZone } from "../ListTenants/types"; import { ITenant, IZone } from "../ListTenants/types";
import Logs from "./Logs/Logs";
import Trace from "./Trace/Trace";
interface ITenantDetailsProps { interface ITenantDetailsProps {
classes: any; classes: any;
@@ -94,8 +95,6 @@ const styles = (theme: Theme) =>
const TenantDetails = ({ classes, match }: ITenantDetailsProps) => { const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
const [selectedTab, setSelectedTab] = useState<number>(0); const [selectedTab, setSelectedTab] = useState<number>(0);
const [capacity, setCapacity] = useState<number>(0); const [capacity, setCapacity] = useState<number>(0);
const [externalIDP, setExternalIDP] = useState<boolean>(false);
const [externalKMS, setExternalKMS] = useState<boolean>(false);
const [zoneCount, setZoneCount] = useState<number>(0); const [zoneCount, setZoneCount] = useState<number>(0);
const [zones, setZones] = useState<IZone[]>([]); const [zones, setZones] = useState<IZone[]>([]);
const [instances, setInstances] = useState<number>(0); const [instances, setInstances] = useState<number>(0);
@@ -103,7 +102,6 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
const [addZoneOpen, setAddZone] = useState<boolean>(false); const [addZoneOpen, setAddZone] = useState<boolean>(false);
const [addBucketOpen, setAddBucketOpen] = useState<boolean>(false); const [addBucketOpen, setAddBucketOpen] = useState<boolean>(false);
const [addReplicationOpen, setAddReplicationOpen] = useState<boolean>(false); const [addReplicationOpen, setAddReplicationOpen] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [tenant, setTenant] = useState<ITenant | null>(null); const [tenant, setTenant] = useState<ITenant | null>(null);
@@ -131,8 +129,6 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
const tenantName = match.params["tenantName"]; const tenantName = match.params["tenantName"];
const tenantNamespace = match.params["tenantNamespace"]; const tenantNamespace = match.params["tenantNamespace"];
setLoading(true);
api api
.invoke( .invoke(
"GET", "GET",
@@ -140,7 +136,7 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
) )
.then((res: ITenant) => { .then((res: ITenant) => {
const resZones = !res.zones ? [] : res.zones; const resZones = !res.zones ? [] : res.zones;
const total = res.volume_count * res.volume_size;
let totalInstances = 0; let totalInstances = 0;
let totalVolumes = 0; let totalVolumes = 0;
let count = 1; let count = 1;
@@ -165,16 +161,15 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
setTenant(res); setTenant(res);
setError(""); setError("");
setLoading(false);
}) })
.catch((err) => { .catch((err) => {
setError(err); setError(err);
setLoading(false);
}); });
}; };
useEffect(() => { useEffect(() => {
loadInfo(); loadInfo();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return ( return (
@@ -204,6 +199,11 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
{`Tenant > ${match.params["tenantName"]}`} {`Tenant > ${match.params["tenantName"]}`}
</Typography> </Typography>
</Grid> </Grid>
{error !== "" && (
<Grid item xs={12}>
{error}
</Grid>
)}
<Grid item xs={12}> <Grid item xs={12}>
<br /> <br />
</Grid> </Grid>
@@ -239,6 +239,8 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
aria-label="tenant-tabs" aria-label="tenant-tabs"
> >
<Tab label="Zones" /> <Tab label="Zones" />
<Tab label="Logs" />
<Tab label="Trace" />
</Tabs> </Tabs>
</Grid> </Grid>
<Grid item xs={6} className={classes.actionsTray}> <Grid item xs={6} className={classes.actionsTray}>
@@ -257,42 +259,50 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
<br /> <br />
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<TableWrapper {selectedTab === 0 && (
itemActions={[ <TableWrapper
{ itemActions={[
type: "delete", {
onClick: (element) => { type: "delete",
console.log(element); onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
}, },
sendOnlyId: true, ]}
}, columns={[
]} { label: "Name", elementKey: "name" },
columns={[ { label: "Capacity", elementKey: "capacity" },
{ label: "Name", elementKey: "name" }, { label: "# of Instances", elementKey: "servers" },
{ label: "Capacity", elementKey: "capacity" }, { label: "# of Drives", elementKey: "volumes" },
{ label: "# of Instances", elementKey: "servers" }, ]}
{ label: "# of Drives", elementKey: "volumes" }, isLoading={false}
]} records={zones}
isLoading={false} entityName="Zones"
records={zones} idField="name"
entityName="Zones" paginatorConfig={{
idField="name" rowsPerPageOptions: [5, 10, 25],
paginatorConfig={{ colSpan: 3,
rowsPerPageOptions: [5, 10, 25], count: zoneCount,
colSpan: 3, rowsPerPage: 10,
count: zoneCount, page: 0,
rowsPerPage: 10, SelectProps: {
page: 0, inputProps: { "aria-label": "rows per page" },
SelectProps: { native: true,
inputProps: { "aria-label": "rows per page" }, },
native: true, ActionsComponent: MinTablePaginationActions,
}, onChangePage: () => {},
ActionsComponent: MinTablePaginationActions, onChangeRowsPerPage: () => {},
onChangePage: () => {}, }}
onChangeRowsPerPage: () => {}, />
}} )}
/>
</Grid> </Grid>
{selectedTab === 1 && tenant !== null && (
<Logs namespace={tenant.namespace} tenant={tenant.name} />
)}
{selectedTab === 2 && tenant !== null && (
<Trace namespace={tenant.namespace} tenant={tenant.name} />
)}
</Grid> </Grid>
</React.Fragment> </React.Fragment>
); );

View File

@@ -16,17 +16,17 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket"; import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { AppState } from "../../../store"; import { AppState } from "../../../../../store";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { traceMessageReceived, traceResetMessages } from "./actions"; import { traceMessageReceived, traceResetMessages } from "./actions";
import { TraceMessage } from "./types"; import { TraceMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { niceBytes, timeFromDate } from "../../../common/utils"; import { niceBytes, timeFromDate } from "../../../../../common/utils";
import { wsProtocol } from "../../../utils/wsUtils"; import { wsProtocol } from "../../../../../utils/wsUtils";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary"; import { containerForHeader } from "../../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader"; import PageHeader from "../../../Common/PageHeader/PageHeader";
import { Grid } from "@material-ui/core"; import { Grid } from "@material-ui/core";
import TableWrapper from "../Common/TableWrapper/TableWrapper"; import TableWrapper from "../../../Common/TableWrapper/TableWrapper";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@@ -45,6 +45,12 @@ const styles = (theme: Theme) =>
borderBottom: "1px solid #dedede", borderBottom: "1px solid #dedede",
}, },
}, },
sizeItem: {
width: 150,
},
timeItem: {
width: 100,
},
...containerForHeader(theme.spacing(4)), ...containerForHeader(theme.spacing(4)),
}); });
@@ -53,6 +59,8 @@ interface ITrace {
traceMessageReceived: typeof traceMessageReceived; traceMessageReceived: typeof traceMessageReceived;
traceResetMessages: typeof traceResetMessages; traceResetMessages: typeof traceResetMessages;
messages: TraceMessage[]; messages: TraceMessage[];
namespace: string;
tenant: string;
} }
const Trace = ({ const Trace = ({
@@ -60,6 +68,8 @@ const Trace = ({
traceMessageReceived, traceMessageReceived,
traceResetMessages, traceResetMessages,
messages, messages,
namespace,
tenant,
}: ITrace) => { }: ITrace) => {
useEffect(() => { useEffect(() => {
traceResetMessages(); traceResetMessages();
@@ -68,7 +78,9 @@ const Trace = ({
const port = isDev ? "9090" : url.port; const port = isDev ? "9090" : url.port;
const wsProt = wsProtocol(url.protocol); const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket(`${wsProt}://${url.hostname}:${port}/ws/trace`); const c = new W3CWebSocket(
`${wsProt}://${url.hostname}:${port}/ws/trace/${namespace}/${tenant}`
);
let interval: any | null = null; let interval: any | null = null;
if (c !== null) { if (c !== null) {
@@ -95,60 +107,62 @@ const Trace = ({
console.log("closing websockets"); console.log("closing websockets");
}; };
} }
}, [traceMessageReceived]); }, [traceMessageReceived, traceResetMessages]);
return ( return (
<React.Fragment> <Grid item xs={12}>
<PageHeader label={"Trace"} /> <TableWrapper
<Grid container> itemActions={[]}
<Grid item xs={12} className={classes.container}> columns={[
<TableWrapper {
itemActions={[]} label: "Time",
columns={[ elementKey: "time",
{ renderFunction: (time: Date) => {
label: "Time", const timeParse = new Date(time);
elementKey: "time", return timeFromDate(timeParse);
renderFunction: (time: Date) => { },
const timeParse = new Date(time); globalClass: classes.timeItem,
return timeFromDate(timeParse); },
}, { label: "Name", elementKey: "api" },
}, {
{ label: "Name", elementKey: "api" }, label: "Status",
{ elementKey: "",
label: "Status", renderFunction: (fullElement: TraceMessage) =>
elementKey: "", `${fullElement.statusCode} ${fullElement.statusMsg}`,
renderFunction: (fullElement: TraceMessage) => renderFullObject: true,
`${fullElement.statusCode} ${fullElement.statusMsg}`, },
renderFullObject: true, {
}, label: "Location",
{ elementKey: "configuration_id",
label: "Location", renderFunction: (fullElement: TraceMessage) =>
elementKey: "configuration_id", `${fullElement.host} ${fullElement.client}`,
renderFunction: (fullElement: TraceMessage) => renderFullObject: true,
`${fullElement.host} ${fullElement.client}`, },
renderFullObject: true, {
}, label: "Load Time",
{ label: "Load Time", elementKey: "callStats.duration" }, elementKey: "callStats.duration",
{ globalClass: classes.timeItem,
label: "Upload", },
elementKey: "callStats.rx", {
renderFunction: niceBytes, label: "Upload",
}, elementKey: "callStats.rx",
{ renderFunction: niceBytes,
label: "Download", globalClass: classes.sizeItem,
elementKey: "callStats.tx", },
renderFunction: niceBytes, {
}, label: "Download",
]} elementKey: "callStats.tx",
isLoading={false} renderFunction: niceBytes,
records={messages} globalClass: classes.sizeItem,
entityName="Traces" },
idField="api" ]}
customEmptyMessage="There are no traced Elements yet" isLoading={false}
/> records={messages}
</Grid> entityName="Traces"
</Grid> idField="api"
</React.Fragment> customEmptyMessage="There are no traced Elements yet"
/>
</Grid>
); );
}; };

View File

@@ -24,7 +24,6 @@ import {
} from "../Common/FormComponents/common/styleLibrary"; } from "../Common/FormComponents/common/styleLibrary";
import api from "../../../common/api"; import api from "../../../common/api";
import GroupsSelectors from "./GroupsSelectors"; import GroupsSelectors from "./GroupsSelectors";
import Title from "../../../common/Title";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper"; import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
interface IAddToGroup { interface IAddToGroup {

View File

@@ -28,7 +28,6 @@ import api from "../../../common/api";
import GroupsSelectors from "./GroupsSelectors"; import GroupsSelectors from "./GroupsSelectors";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper"; import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
const styles = (theme: Theme) => const styles = (theme: Theme) =>

View File

@@ -19,7 +19,6 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { LinearProgress } from "@material-ui/core"; import { LinearProgress } from "@material-ui/core";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Title from "../../../common/Title";
import InputAdornment from "@material-ui/core/InputAdornment"; import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";

View File

@@ -17,13 +17,7 @@
import React from "react"; import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../common/api"; import api from "../../../common/api";
import { import { Button, Grid, TextField, InputAdornment } from "@material-ui/core";
Button,
Grid,
Typography,
TextField,
InputAdornment,
} from "@material-ui/core";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search";
import GroupIcon from "@material-ui/icons/Group"; import GroupIcon from "@material-ui/icons/Group";
import { User, UsersList } from "./types"; import { User, UsersList } from "./types";
@@ -34,7 +28,6 @@ import AddUser from "./AddUser";
import DeleteUser from "./DeleteUser"; import DeleteUser from "./DeleteUser";
import AddToGroup from "./AddToGroup"; import AddToGroup from "./AddToGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper"; import TableWrapper from "../Common/TableWrapper/TableWrapper";
import DescriptionIcon from "@material-ui/icons/Description";
import SetPolicy from "../Policies/SetPolicy"; import SetPolicy from "../Policies/SetPolicy";
import { import {
actionsTray, actionsTray,
@@ -183,7 +176,6 @@ class Users extends React.Component<IUsersProps, IUsersState> {
const { classes } = this.props; const { classes } = this.props;
const { const {
records, records,
totalRecords,
addScreenOpen, addScreenOpen,
loading, loading,
page, page,

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button, Grid, Typography, TextField } from "@material-ui/core"; import { Button, Grid, TextField, InputBase } from "@material-ui/core";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket"; import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { AppState } from "../../../store"; import { AppState } from "../../../store";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -25,8 +25,13 @@ import { niceBytes, timeFromDate } from "../../../common/utils";
import { wsProtocol } from "../../../utils/wsUtils"; import { wsProtocol } from "../../../utils/wsUtils";
import api from "../../../common/api"; import api from "../../../common/api";
import { FormControl, MenuItem, Select } from "@material-ui/core"; import { FormControl, MenuItem, Select } from "@material-ui/core";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary"; import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader"; import PageHeader from "../Common/PageHeader/PageHeader";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@@ -45,31 +50,34 @@ const styles = (theme: Theme) =>
borderBottom: "1px solid #dedede", borderBottom: "1px solid #dedede",
}, },
}, },
actionsTray: { searchPrefix: {
textAlign: "right", flexGrow: 1,
"& button": { marginLeft: 15,
marginLeft: 10,
},
},
inputField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
marginLeft: 10,
boxShadow: "0px 3px 6px #00000012",
},
fieldContainer: {
background: "#FFFFFF",
padding: 0,
borderRadius: 5,
marginLeft: 10,
textAlign: "left",
minWidth: "206px",
boxShadow: "0px 3px 6px #00000012",
}, },
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)), ...containerForHeader(theme.spacing(4)),
}); });
const SelectStyled = withStyles((theme: Theme) =>
createStyles({
root: {
width: 450,
lineHeight: 1,
"label + &": {
marginTop: theme.spacing(3),
},
"& .MuiSelect-select:focus": {
backgroundColor: "transparent",
},
},
input: {
fontSize: 13,
lineHeight: 15,
},
})
)(InputBase);
interface IWatch { interface IWatch {
classes: any; classes: any;
watchMessageReceived: typeof watchMessageReceived; watchMessageReceived: typeof watchMessageReceived;
@@ -150,12 +158,21 @@ const Watch = ({
// reset start status // reset start status
setStart(false); setStart(false);
} }
}, [watchMessageReceived, start]); }, [
watchMessageReceived,
start,
bucketList,
bucketName,
prefix,
suffix,
watchResetMessages,
]);
const bucketNames = bucketList.map((bucketName) => ({ const bucketNames = bucketList.map((bucketName) => ({
label: bucketName.name, label: bucketName.name,
value: bucketName.name, value: bucketName.name,
})); }));
return ( return (
<React.Fragment> <React.Fragment>
<PageHeader label="Watch" /> <PageHeader label="Watch" />
@@ -170,8 +187,9 @@ const Watch = ({
onChange={(e) => { onChange={(e) => {
setBucketName(e.target.value as string); setBucketName(e.target.value as string);
}} }}
className={classes.fieldContainer} className={classes.searchField}
disabled={start} disabled={start}
input={<SelectStyled />}
> >
<MenuItem <MenuItem
value={bucketName} value={bucketName}
@@ -192,7 +210,7 @@ const Watch = ({
</FormControl> </FormControl>
<TextField <TextField
placeholder="Prefix" placeholder="Prefix"
className={classes.inputField} className={`${classes.searchField} ${classes.searchPrefix}`}
id="prefix-resource" id="prefix-resource"
label="" label=""
disabled={start} disabled={start}
@@ -205,7 +223,7 @@ const Watch = ({
/> />
<TextField <TextField
placeholder="Suffix" placeholder="Suffix"
className={classes.inputField} className={`${classes.searchField} ${classes.searchPrefix}`}
id="suffix-resource" id="suffix-resource"
label="" label=""
disabled={start} disabled={start}
@@ -229,18 +247,27 @@ const Watch = ({
<Grid item xs={12}> <Grid item xs={12}>
<br /> <br />
</Grid> </Grid>
<div className={classes.watchList}> <TableWrapper
<ul> columns={[
{messages.map((m) => { {
return ( label: "Time",
<li key={m.key}> elementKey: "Time",
{timeFromDate(m.Time)} - {niceBytes(m.Size + "")} - {m.Type}{" "} renderFunction: timeFromDate,
- {m.Path} },
</li> {
); label: "Size",
})} elementKey: "Size",
</ul> renderFunction: niceBytes,
</div> },
{ label: "Type", elementKey: "Type" },
{ label: "Path", elementKey: "Path" },
]}
records={messages}
entityName={"Watch"}
customEmptyMessage={"No Changes at this time"}
idField={"watch_table"}
isLoading={false}
/>
</Grid> </Grid>
</Grid> </Grid>
</React.Fragment> </React.Fragment>

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { FC, useEffect } from "react"; import React, { FC, useEffect } from "react"; // eslint-disable-line @typescript-eslint/no-unused-vars
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
import storage from "local-storage-fallback"; import storage from "local-storage-fallback";
import api from "../../common/api"; import api from "../../common/api";
@@ -36,6 +36,7 @@ const LoginCallback: FC<RouteComponentProps> = ({ location }) => {
.catch((res: any) => { .catch((res: any) => {
window.location.href = "/login"; window.location.href = "/login";
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return null; return null;
}; };

View File

@@ -16,22 +16,32 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import request from "superagent"; import request from "superagent";
import storage from "local-storage-fallback";
import { connect, ConnectedProps } from "react-redux"; import { connect, ConnectedProps } from "react-redux";
import ErrorIcon from "@material-ui/icons/Error"; import ErrorIcon from "@material-ui/icons/Error";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import { CircularProgress, Paper } from "@material-ui/core"; import {
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; CircularProgress,
LinearProgress,
Paper,
TextFieldProps,
} from "@material-ui/core";
import {
createStyles,
makeStyles,
Theme,
withStyles,
} from "@material-ui/core/styles";
import { SystemState } from "../../types"; import { SystemState } from "../../types";
import { userLoggedIn } from "../../actions"; import { userLoggedIn } from "../../actions";
import api from "../../common/api"; import api from "../../common/api";
import { ILoginDetails, loginStrategyType } from "./types"; import { ILoginDetails, loginStrategyType } from "./types";
import { setSession } from "../../common/utils"; import { setSession } from "../../common/utils";
import history from "../../history"; import history from "../../history";
import { Error } from "@material-ui/icons"; import { isBoolean } from "util";
import { OutlinedInputProps } from "@material-ui/core/OutlinedInput";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@@ -123,8 +133,33 @@ const styles = (theme: Theme) =>
jwtInput: { jwtInput: {
marginTop: 45, marginTop: 45,
}, },
linearPredef: {
height: 10,
},
}); });
const inputStyles = makeStyles((theme: Theme) =>
createStyles({
disabled: {
"&.MuiInput-underline::before": {
borderColor: "#eaeaea",
borderBottomStyle: "solid",
},
},
})
);
function LoginField(props: TextFieldProps) {
const classes = inputStyles();
return (
<TextField
InputProps={{ classes } as Partial<OutlinedInputProps>}
{...props}
/>
);
}
const mapState = (state: SystemState) => ({ const mapState = (state: SystemState) => ({
loggedIn: state.loggedIn, loggedIn: state.loggedIn,
}); });
@@ -154,11 +189,11 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
const [jwt, setJwt] = useState<string>(""); const [jwt, setJwt] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>(""); const [secretKey, setSecretKey] = useState<string>("");
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [loginStrategy, setLoginStrategy] = useState<ILoginDetails>({ const [loginStrategy, setLoginStrategy] = useState<ILoginDetails>({
loginStrategy: loginStrategyType.unknown, loginStrategy: loginStrategyType.unknown,
redirect: "", redirect: "",
}); });
const [loginSending, setLoginSending] = useState<boolean>(false);
const loginStrategyEndpoints: LoginStrategyRoutes = { const loginStrategyEndpoints: LoginStrategyRoutes = {
form: "/api/v1/login", form: "/api/v1/login",
@@ -170,23 +205,20 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
}; };
const fetchConfiguration = () => { const fetchConfiguration = () => {
setLoading(true);
api api
.invoke("GET", "/api/v1/login") .invoke("GET", "/api/v1/login")
.then((loginDetails: ILoginDetails) => { .then((loginDetails: ILoginDetails) => {
setLoading(false);
setLoginStrategy(loginDetails); setLoginStrategy(loginDetails);
setError(""); setError("");
}) })
.catch((err: any) => { .catch((err: any) => {
setLoading(false);
setError(err); setError(err);
}); });
}; };
const formSubmit = (e: React.FormEvent<HTMLFormElement>) => { const formSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setLoginSending(true);
request request
.post( .post(
loginStrategyEndpoints[loginStrategy.loginStrategy] || "/api/v1/login" loginStrategyEndpoints[loginStrategy.loginStrategy] || "/api/v1/login"
@@ -198,6 +230,7 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
// store the jwt token // store the jwt token
setSession(bodyResponse.sessionId); setSession(bodyResponse.sessionId);
} else if (bodyResponse.error) { } else if (bodyResponse.error) {
setLoginSending(false);
// throw will be moved to catch block once bad login returns 403 // throw will be moved to catch block once bad login returns 403
throw bodyResponse.error; throw bodyResponse.error;
} }
@@ -208,6 +241,7 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
history.push("/"); history.push("/");
}) })
.catch((err) => { .catch((err) => {
setLoginSending(false);
setError(err.message); setError(err.message);
}); });
}; };
@@ -232,7 +266,7 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
<form className={classes.form} noValidate onSubmit={formSubmit}> <form className={classes.form} noValidate onSubmit={formSubmit}>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12}> <Grid item xs={12}>
<TextField <LoginField
fullWidth fullWidth
id="accessKey" id="accessKey"
value={accessKey} value={accessKey}
@@ -242,10 +276,11 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
label="Enter Access Key" label="Enter Access Key"
name="accessKey" name="accessKey"
autoComplete="username" autoComplete="username"
disabled={loginSending}
/> />
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<TextField <LoginField
fullWidth fullWidth
value={secretKey} value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
@@ -256,6 +291,7 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
type="password" type="password"
id="secretKey" id="secretKey"
autoComplete="current-password" autoComplete="current-password"
disabled={loginSending}
/> />
</Grid> </Grid>
</Grid> </Grid>
@@ -265,11 +301,14 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
variant="contained" variant="contained"
color="primary" color="primary"
className={classes.submit} className={classes.submit}
disabled={secretKey === "" || accessKey === ""} disabled={secretKey === "" || accessKey === "" || loginSending}
> >
Login Login
</Button> </Button>
</Grid> </Grid>
<Grid item xs={12} className={classes.linearPredef}>
{loginSending && <LinearProgress />}
</Grid>
<Grid item xs={12} className={classes.disclaimer}> <Grid item xs={12} className={classes.disclaimer}>
<strong>Don't have an access key?</strong> <strong>Don't have an access key?</strong>
<br /> <br />
@@ -321,7 +360,7 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
<form className={classes.form} noValidate onSubmit={formSubmit}> <form className={classes.form} noValidate onSubmit={formSubmit}>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12} className={classes.jwtInput}> <Grid item xs={12} className={classes.jwtInput}>
<TextField <LoginField
required required
fullWidth fullWidth
id="jwt" id="jwt"
@@ -332,6 +371,7 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
label="JWT" label="JWT"
name="jwt" name="jwt"
autoComplete="Service Account JWT Token" autoComplete="Service Account JWT Token"
disabled={loginSending}
/> />
</Grid> </Grid>
</Grid> </Grid>
@@ -341,11 +381,14 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
variant="contained" variant="contained"
color="primary" color="primary"
className={classes.submit} className={classes.submit}
disabled={jwt === ""} disabled={jwt === "" || loginSending}
> >
Login Login
</Button> </Button>
</Grid> </Grid>
<Grid item xs={12} className={classes.linearPredef}>
{loginSending && <LinearProgress />}
</Grid>
<Grid item xs={12} className={classes.disclaimer}> <Grid item xs={12} className={classes.disclaimer}>
<strong>Don't have an access key?</strong> <strong>Don't have an access key?</strong>
<br /> <br />

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