Compare commits

..

34 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
Minio Trusted
716f886780 update to v0.4.2 2020-10-22 15:35:17 -07:00
Alex
4ef498f0c3 Updated Logs page to be more consistent with current styles (#338)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-10-22 12:08:36 -07:00
Alex
5e764e61ba Changed trace view to be a table (#337)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-10-22 11:27:24 -07:00
Cesar N
1466632fd6 Add share object api (#335) 2020-10-22 11:18:27 -07:00
Lenin Alevski
0c43e5c3f4 React Router fixes for Console (#336)
- Adding protectedRoute component
- Removed unnecessary redirect login
2020-10-21 13:13:40 -07:00
Alex
7e9d581277 Updated styles & behavior for settings page (#334)
Updated styles & behavior for settings page, also implemented a couple of performance improvements on some fields

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-10-20 16:31:08 -07:00
Cesar N
c928972137 Change Users label to Tenants on Tenants Page (#330) 2020-10-20 11:24:52 -07:00
Daniel Valdivia
78884e3806 Make logs, trace and watch have fixed height (#333) 2020-10-20 09:06:23 -07:00
Lenin Alevski
f6ac7e047e Invalidate console session when minio user doesn't exists (#332) 2020-10-19 15:32:21 -07:00
Alex
e1fdf3fb28 Modals UI style changes (#331)
Implements new input styles & adjusts information on modal boxes for console.

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-10-19 11:27:54 -07:00
Cesar N
e4510cbc18 Add upload api and integrate it with object browser on UI (#327) 2020-10-14 23:09:33 -07:00
182 changed files with 13430 additions and 6244 deletions

View File

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

View File

@@ -12,6 +12,10 @@ before:
hooks:
# you may remove this if you don't use vgo
- 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:
-

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
@@ -12,6 +33,8 @@ RUN go mod download
ADD . /go/src/github.com/minio/console/
WORKDIR /go/src/github.com/minio/console/
COPY --from=uilayer /app/bindata_assetfs.go /go/src/github.com/minio/console/portal-ui/
ENV CGO_ENABLED=0
RUN go build -ldflags "-w -s" -a -o console ./cmd/console
@@ -20,7 +43,7 @@ FROM scratch
MAINTAINER MinIO Development "dev@min.io"
EXPOSE 9090
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=0 /go/src/github.com/minio/console/console .
COPY --from=golayer /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=golayer /go/src/github.com/minio/console/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

@@ -101,8 +101,6 @@ Additionally, you can create policies to limit the privileges for `console` user
To run the server:
```
export CONSOLE_HMAC_JWT_SECRET=YOURJWTSIGNINGSECRET
#required to encrypt jwet payload
export CONSOLE_PBKDF_PASSPHRASE=SECRET
@@ -115,11 +113,41 @@ export CONSOLE_MINIO_SERVER=http://localhost:9000
./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
./console server
```

File diff suppressed because one or more lines are too long

View File

@@ -20,12 +20,16 @@ import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/go-openapi/loads"
"github.com/jessevdk/go-flags"
"github.com/minio/cli"
"github.com/minio/console/pkg/certs"
"github.com/minio/console/restapi"
"github.com/minio/console/restapi/operations"
"github.com/minio/minio/cmd/logger"
certsx "github.com/minio/minio/pkg/certs"
)
// starts the server
@@ -56,14 +60,9 @@ var serverCmd = cli.Command{
Usage: "HTTPS server port",
},
cli.StringFlag{
Name: "tls-certificate",
Value: "",
Usage: "filename of public cert",
},
cli.StringFlag{
Name: "tls-key",
Value: "",
Usage: "filename of private key",
Name: "certs-dir",
Value: certs.GlobalCertsCADir.Get(),
Usage: "path to certs directory",
},
},
}
@@ -82,7 +81,9 @@ func startServer(ctx *cli.Context) error {
parser := flags.NewParser(server, flags.Default)
parser.ShortDescription = "MinIO Console Server"
parser.LongDescription = swaggerSpec.Spec().Info.Description
server.ConfigureFlags()
for _, optsGroup := range api.CommandLineOptionsGroups {
_, err := parser.AddGroup(optsGroup.ShortDescription, optsGroup.LongDescription, optsGroup.Options)
if err != nil {
@@ -106,12 +107,19 @@ func startServer(ctx *cli.Context) error {
restapi.Hostname = ctx.String("host")
restapi.Port = fmt.Sprintf("%v", ctx.Int("port"))
tlsCertificatePath := ctx.String("tls-certificate")
tlsCertificateKeyPath := ctx.String("tls-key")
// Set all certs and CAs directories.
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 != "" {
server.TLSCertificate = flags.Filename(tlsCertificatePath)
server.TLSCertificateKey = flags.Filename(tlsCertificateKeyPath)
// load all CAs from ~/.console/certs/CAs
restapi.GlobalRootCAs, err = certsx.GetRootCAs(certs.GlobalCertsCADir.Get())
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
// plain HTTP connections to HTTPS server
server.EnabledListeners = []string{"http", "https"}

View File

@@ -2,7 +2,7 @@
`Console` will authenticate against `Kubernetes`using bearer tokens via HTTP `Authorization` header. The user will provide this token once
in the login form, Console will validate it against Kubernetes (list apis) and if valid will generate and return a new Console sessions
with encrypted claims (the user Service account token will be inside the JWT in the data field)
with encrypted claims (the user Service account token will be inside the session encrypted token
# Kubernetes

5
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/minio/console
go 1.13
go 1.15
require (
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/minio v0.0.0-20200927172404-27d9bd04e544
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/secure-io/sio-go v0.3.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-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.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/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.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/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU=
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.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.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/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/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.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/mocks v0.1.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/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/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/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/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
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/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
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.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/operator v0.0.0-20200930213302-ab2bbdfae96c h1:OIKdzEJDFmUokbJ1rIdlr3kcfsBfXelYgSCTN/+Ppec=
github.com/minio/operator v0.0.0-20200930213302-ab2bbdfae96c/go.mod h1:6lavbNo2YuJWeQR5bZYsEWdbpRCO2KrTyfQ0PtC/AN4=
github.com/minio/operator v0.0.0-20201022162018-527e5c32132b h1:ggfD6V3nodTzhHJHCYIT1F897gscrz+hHsan0a2Wtqw=
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/go.mod h1:b8ThJzzH7u2MkF6PcIra7KaXO9Khf6alWPvMSyTDCFM=
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=

View File

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

View File

@@ -15,7 +15,7 @@ spec:
serviceAccountName: console-sa
containers:
- name: console
image: minio/console:v0.4.1
image: minio/console:v0.4.4
imagePullPolicy: "IfNotPresent"
env:
- 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 int64 `json:"size,omitempty"`
// tags
Tags map[string]string `json:"tags,omitempty"`
// user tags
UserTags map[string]string `json:"user_tags,omitempty"`

View File

@@ -93,9 +93,6 @@ type CreateTenantRequest struct {
// secret key
SecretKey string `json:"secret_key,omitempty"`
// service name
ServiceName string `json:"service_name,omitempty"`
// tls
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
var (
configuration = "/configurations-list"
users = "/users"
groups = "/groups"
iamPolicies = "/policies"
dashboard = "/dashboard"
profiling = "/profiling"
trace = "/trace"
logs = "/logs"
watch = "/watch"
notifications = "/notification-endpoints"
buckets = "/buckets"
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/service-accounts"
tenants = "/tenants"
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
heal = "/heal"
remoteBuckets = "/remote-buckets"
replication = "/replication"
objectBrowser = "/object-browser/:bucket?"
mainObjectBrowser = "/object-browser"
license = "/license"
configuration = "/configurations-list"
users = "/users"
groups = "/groups"
iamPolicies = "/policies"
dashboard = "/dashboard"
profiling = "/profiling"
watch = "/watch"
notifications = "/notification-endpoints"
buckets = "/buckets"
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/service-accounts"
tenants = "/tenants"
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
heal = "/heal"
remoteBuckets = "/remote-buckets"
replication = "/replication"
objectBrowser = "/object-browser/:bucket/*"
objectBrowserBucket = "/object-browser/:bucket"
mainObjectBrowser = "/object-browser"
license = "/license"
)
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
var dashboardActionSet = ConfigurationActionSet{
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
var usersActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
@@ -245,25 +224,24 @@ var licenseActionSet = ConfigurationActionSet{
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet,
users: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
dashboard: dashboardActionSet,
profiling: profilingActionSet,
trace: traceActionSet,
logs: logsActionSet,
watch: watchActionSet,
notifications: notificationsActionSet,
buckets: bucketsActionSet,
bucketsDetail: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
heal: healActionSet,
remoteBuckets: remoteBucketsActionSet,
replication: replicationActionSet,
objectBrowser: objectBrowserActionSet,
mainObjectBrowser: objectBrowserActionSet,
license: licenseActionSet,
configuration: configurationActionSet,
users: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
dashboard: dashboardActionSet,
profiling: profilingActionSet,
watch: watchActionSet,
notifications: notificationsActionSet,
buckets: bucketsActionSet,
bucketsDetail: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
heal: healActionSet,
remoteBuckets: remoteBucketsActionSet,
replication: replicationActionSet,
objectBrowser: objectBrowserActionSet,
mainObjectBrowser: objectBrowserActionSet,
objectBrowserBucket: objectBrowserActionSet,
license: licenseActionSet,
}
// 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{
[]string{"admin:ServerInfo"},
},
want: 5,
want: 6,
},
{
name: "policies endpoint",
@@ -63,7 +63,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:ListUserPolicies",
},
},
want: 5,
want: 6,
},
{
name: "all admin endpoints",
@@ -72,7 +72,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:*",
},
},
want: 16,
want: 15,
},
{
name: "all s3 endpoints",
@@ -81,7 +81,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 7,
want: 8,
},
{
name: "all admin and s3 endpoints",
@@ -91,7 +91,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 19,
want: 18,
},
{
name: "no endpoints",

View File

@@ -23,10 +23,9 @@ import (
"github.com/minio/minio/pkg/env"
)
// ConsoleSTSAndJWTDurationSeconds returns the default session duration for the STS requested tokens and the generated JWTs.
// Ideally both values should match so jwt and Minio sts sessions expires at the same time.
func GetConsoleSTSAndJWTDurationInSeconds() int {
duration, err := strconv.Atoi(env.Get(ConsoleSTSAndJWTDurationSeconds, "3600"))
// ConsoleSTSDurationSeconds returns the default session duration for the STS requested tokens.
func GetConsoleSTSDurationInSeconds() int {
duration, err := strconv.Atoi(env.Get(ConsoleSTSDurationSeconds, "3600"))
if err != nil {
duration = 3600
}

View File

@@ -17,7 +17,7 @@
package token
const (
ConsoleSTSAndJWTDurationSeconds = "CONSOLE_STS_AND_JWT_DURATION_SECONDS"
ConsolePBKDFPassphrase = "CONSOLE_PBKDF_PASSPHRASE"
ConsolePBKDFSalt = "CONSOLE_PBKDF_SALT"
ConsoleSTSDurationSeconds = "CONSOLE_STS_DURATION_SECONDS"
ConsolePBKDFPassphrase = "CONSOLE_PBKDF_PASSPHRASE"
ConsolePBKDFSalt = "CONSOLE_PBKDF_SALT"
)

View File

@@ -60,17 +60,17 @@ func TestJWTAuthenticate(t *testing.T) {
funcAssert.Equal(claims.SecretAccessKey, creds.SecretAccessKey)
funcAssert.Equal(claims.SessionToken, creds.SessionToken)
}
// Test-2 : SessionTokenAuthenticate() return an error because of a tampered jwt
// Test-2 : SessionTokenAuthenticate() return an error because of a tampered token
if _, err := SessionTokenAuthenticate(badToken); err != nil {
funcAssert.Equal("session token internal data is malformed", err.Error())
}
// Test-3 : SessionTokenAuthenticate() return an error because of an empty jwt
// Test-3 : SessionTokenAuthenticate() return an error because of an empty token
if _, err := SessionTokenAuthenticate(""); err != nil {
funcAssert.Equal("session token missing", err.Error())
}
}
func TestIsJWTValid(t *testing.T) {
func TestSessionTokenValid(t *testing.T) {
funcAssert := assert.New(t)
// Test-1 : SessionTokenAuthenticate() provided token is valid
funcAssert.Equal(true, IsSessionTokenValid(goodToken))

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",
"lodash": "^4.17.19",
"moment": "^2.24.0",
"npm": "^6.14.4",
"react": "^16.13.1",
"react-app-rewire-hot-loader": "^2.0.1",
"react-app-rewired": "^2.1.6",
@@ -40,7 +39,7 @@
"react-moment": "^0.9.7",
"react-redux": "^7.1.3",
"react-router-dom": "^5.1.2",
"react-scripts": "3.4.1",
"react-scripts": "3.4.4",
"recharts": "^1.8.5",
"redux": "^4.0.4",
"redux-thunk": "^2.3.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -19,7 +19,6 @@ import { Redirect, Route, Router, Switch } from "react-router-dom";
import history from "./history";
import Login from "./screens/LoginPage/LoginPage";
import Console from "./screens/Console/Console";
import NotFoundPage from "./screens/NotFoundPage";
import storage from "local-storage-fallback";
import { connect } from "react-redux";
import { AppState } from "./store";
@@ -27,6 +26,22 @@ import { userLoggedIn } from "./actions";
import LoginCallback from "./screens/LoginPage/LoginCallback";
import { hot } from "react-hot-loader/root";
interface ProtectedRouteProps {
loggedIn: boolean;
component: any;
}
export class ProtectedRoute extends React.Component<ProtectedRouteProps> {
render() {
const Component = this.props.component;
return this.props.loggedIn ? (
<Component />
) : (
<Redirect to={{ pathname: "/login" }} />
);
}
}
const isLoggedIn = () => {
return (
storage.getItem("token") !== undefined &&
@@ -47,29 +62,14 @@ interface RoutesProps {
}
class Routes extends React.Component<RoutesProps> {
componentDidMount(): void {
if (isLoggedIn()) {
this.props.userLoggedIn(true);
}
}
render() {
const loggedIn = isLoggedIn();
return (
<Router history={history}>
<Switch>
<Route exact path="/oauth_callback" component={LoginCallback} />
<Route exact path="/login" component={Login} />
{this.props.loggedIn ? (
<Switch>
<Route path="/*" component={Console} />
<Route component={NotFoundPage} />
</Switch>
) : (
<Switch>
<Route exact path="/" component={Login} />
<Redirect to="/" />
</Switch>
)}
<ProtectedRoute component={Console} loggedIn={loggedIn} />
</Switch>
</Router>
);

View File

@@ -15,7 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
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 minMemReq = 2147483648; // Minimal Memory required for MinIO in bytes

View File

@@ -0,0 +1,34 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { SvgIcon } from "@material-ui/core";
class AddIcon extends React.Component {
render() {
return (
<SvgIcon viewBox="0 0 12 12">
<path
fill="#081c42"
className="a"
d="M-13160.269,1885.114h-3.235v-4.381h-4.382V1877.5h4.382v-4.381h3.235v4.381h4.383v3.238h-4.383v4.38Z"
transform="translate(13167.886 -1873.114)"
/>
</SvgIcon>
);
}
}
export default AddIcon;

View File

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

View File

@@ -0,0 +1,33 @@
// 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 RemoveIcon extends React.Component {
render() {
return (
<SvgIcon viewBox="0 0 11.656 3.101">
<path
fill="#081c42"
d="M-13157.172,1879.551h-11.656v-3.1h11.656v3.1Z"
transform="translate(13168.828 -1876.449)"
/>
</SvgIcon>
);
}
}
export default RemoveIcon;

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 ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import CheckboxWrapper from "../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { factorForDropdown, getBytes } from "../../../../common/utils";
import { AppState } from "../../../../store";
@@ -68,7 +67,7 @@ const styles = (theme: Theme) =>
interface IAddBucketProps {
classes: any;
open: boolean;
closeModalAndRefresh: () => void;
closeModalAndRefresh: (refresh: boolean) => void;
addBucketName: typeof addBucketName;
addBucketVersioned: typeof addBucketVersioned;
addBucketQuota: typeof addBucketQuota;
@@ -103,6 +102,7 @@ const AddBucket = ({
const [bName, setBName] = useState<string>(bucketName);
const [addLoading, setAddLoading] = useState<boolean>(false);
const [addError, setAddError] = useState<string>("");
const [sendEnabled, setSendEnabled] = useState<boolean>(false);
const addRecord = (event: React.FormEvent) => {
event.preventDefault();
@@ -130,7 +130,7 @@ const AddBucket = ({
.then((res) => {
setAddLoading(false);
setAddError("");
closeModalAndRefresh();
closeModalAndRefresh(true);
})
.catch((err) => {
setAddLoading(false);
@@ -141,9 +141,33 @@ const AddBucket = ({
const [value] = useDebounce(bName, 1000);
useEffect(() => {
console.log("called");
addBucketName(value);
}, [value]);
}, [value, addBucketName]);
const resetForm = () => {
setBName("");
addBucketVersioned(false);
addBucketQuota(false);
addBucketQuotaType("hard");
addBucketQuotaSize("1");
addBucketQuotaUnit("TiB");
};
useEffect(() => {
let valid = false;
if (bName.trim() !== "") {
valid = true;
}
if (enableQuota && valid) {
if (quotaSize.trim() === "" || parseInt(quotaSize) === 0) {
valid = false;
}
}
setSendEnabled(valid);
}, [bName, versioned, quotaType, quotaSize, quotaUnit, enableQuota]);
return (
<ModalWrapper
@@ -151,7 +175,7 @@ const AddBucket = ({
modalOpen={open}
onClose={() => {
setAddError("");
closeModalAndRefresh();
closeModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
@@ -196,7 +220,8 @@ const AddBucket = ({
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
addBucketVersioned(event.target.checked);
}}
label={"Turn On Versioning"}
label={"Versioning"}
indicatorLabel={"On"}
/>
</Grid>
<Grid item xs={12}>
@@ -209,6 +234,7 @@ const AddBucket = ({
addBucketQuota(event.target.checked);
}}
label={"Enable Bucket Quota"}
indicatorLabel={"On"}
/>
</Grid>
{enableQuota && (
@@ -264,11 +290,19 @@ const AddBucket = ({
)}
</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={addLoading}
disabled={addLoading || !sendEnabled}
>
Save
</Button>

View File

@@ -18,7 +18,6 @@ import React, { useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
@@ -93,7 +92,6 @@ const ListBuckets = ({
const [totalRecords, setTotalRecords] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [deleteError, setDeleteError] = useState<string>("");
const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
@@ -136,10 +134,13 @@ const ListBuckets = ({
}
}, [loading, page, rowsPerPage]);
const closeAddModalAndRefresh = () => {
const closeAddModalAndRefresh = (refresh: boolean) => {
addBucketOpen(false);
addBucketReset();
setLoading(true);
if (refresh) {
setLoading(true);
}
};
const closeDeleteModalAndRefresh = (refresh: boolean) => {
@@ -184,28 +185,26 @@ const ListBuckets = ({
return <Moment>{date}</Moment>;
};
const filteredRecords = records
.filter((b: Bucket) => {
if (filterBuckets === "") {
const filteredRecords = records.filter((b: Bucket) => {
if (filterBuckets === "") {
return true;
} else {
if (b.name.indexOf(filterBuckets) >= 0) {
return true;
} else {
if (b.name.indexOf(filterBuckets) >= 0) {
return true;
} else {
return false;
}
return false;
}
})
.slice(offset, offset + rowsPerPage);
}
});
const showInPage = filteredRecords.slice(offset, offset + rowsPerPage);
return (
<React.Fragment>
{addBucketModalOpen && (
<AddBucket
open={addBucketModalOpen}
closeModalAndRefresh={() => {
closeAddModalAndRefresh();
}}
closeModalAndRefresh={closeAddModalAndRefresh}
/>
)}
{deleteOpen && (
@@ -219,6 +218,7 @@ const ListBuckets = ({
)}
<PageHeader label={"Buckets"} />
<Grid container>
{error !== "" && <span className={classes.error}>{error}</span>}
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
@@ -269,13 +269,13 @@ const ListBuckets = ({
},
]}
isLoading={loading}
records={filteredRecords}
records={showInPage}
entityName="Buckets"
idField="name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
count: filteredRecords.length,
rowsPerPage: rowsPerPage,
page: page,
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,29 +14,48 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { BucketObject, BucketObjectsList } from "./types";
import api from "../../../../../../common/api";
import React, { useEffect, useState } from "react";
import TableWrapper from "../../../../Common/TableWrapper/TableWrapper";
import { MinTablePaginationActions } from "../../../../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../.././../../../icons";
import { niceBytes } from "../../../../../../common/utils";
import Moment from "react-moment";
import DeleteObject from "./DeleteObject";
import {
actionsTray,
containerForHeader,
objectBrowserCommon,
searchField,
} from "../../../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../../../Common/PageHeader/PageHeader";
import storage from "local-storage-fallback";
import { isNullOrUndefined } from "util";
import { Button, Input } from "@material-ui/core";
import * as reactMoment from "react-moment";
import { CreateIcon } from "../../../../../../icons";
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) =>
createStyles({
@@ -68,84 +87,181 @@ 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,
...searchField,
...objectBrowserCommon,
...containerForHeader(theme.spacing(4)),
});
interface IListObjectsProps {
classes: any;
match: any;
addRoute: (param1: string, param2: string) => any;
setAllRoutes: (path: string) => any;
routesList: Route[];
}
interface IListObjectsState {
records: BucketObject[];
totalRecords: number;
loading: boolean;
error: string;
deleteOpen: boolean;
deleteError: string;
selectedObject: string;
selectedBucket: string;
filterObjects: string;
interface ObjectBrowserReducer {
objectBrowser: ObjectBrowserState;
}
class ListObjects extends React.Component<
IListObjectsProps,
IListObjectsState
> {
state: IListObjectsState = {
records: [],
totalRecords: 0,
loading: false,
error: "",
deleteOpen: false,
deleteError: "",
selectedObject: "",
selectedBucket: "",
filterObjects: "",
const ListObjects = ({
classes,
match,
addRoute,
setAllRoutes,
routesList,
}: IListObjectsProps) => {
const [records, setRecords] = useState<BucketObject[]>([]);
const [totalRecords, setTotalRecords] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [createFolderOpen, setCreateFolderOpen] = useState<boolean>(false);
const [deleteError, setDeleteError] = useState<string>("");
const [selectedObject, setSelectedObject] = useState<string>("");
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 = () => {
this.setState({ loading: true }, () => {
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 });
});
});
const closeAddFolderModal = () => {
setCreateFolderOpen(false);
};
componentDidMount(): void {
this.fetchRecords();
}
const showSnackBarMessage = (text: string) => {
setSnackbarMessage(text);
setOpenSnackbar(true);
};
closeDeleteModalAndRefresh(refresh: boolean) {
this.setState({ deleteOpen: false }, () => {
if (refresh) {
this.fetchRecords();
const closeSnackBar = () => {
setSnackbarMessage("");
setOpenSnackbar(false);
};
const upload = (e: any, bucketName: string, path: string) => {
if (isNullOrUndefined(e) || isNullOrUndefined(e.target)) {
return;
}
const token: string = storage.getItem("token")!;
e.preventDefault();
let file = e.target.files[0];
const fileName = file.name;
const objectName = `${path}${fileName}`;
let uploadUrl = `/api/v1/buckets/${bucketName}/objects/upload?prefix=${objectName}`;
let xhr = new XMLHttpRequest();
xhr.open("POST", uploadUrl, true);
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
xhr.withCredentials = false;
xhr.onload = function (event) {
// TODO: handle status
if (xhr.status === 401 || xhr.status === 403) {
showSnackBarMessage("An error occurred while uploading the file.");
}
});
}
if (xhr.status === 500) {
showSnackBarMessage("An error occurred while uploading the file.");
}
if (xhr.status === 200) {
showSnackBarMessage("Object uploaded successfully.");
setLoading(true);
}
};
download(bucketName: string, objectName: string) {
var anchor = document.createElement("a");
xhr.upload.addEventListener("error", (event) => {
// TODO: handle error
showSnackBarMessage("An error occurred while uploading the file.");
});
xhr.upload.addEventListener("progress", (event) => {
// TODO: handle progress with event.loaded, event.total
});
xhr.onerror = () => {
showSnackBarMessage("An error occurred while uploading the file.");
};
const formData = new FormData();
const blobFile = new Blob([file]);
formData.append("upfile", blobFile);
xhr.send(formData);
e.target.value = null;
};
const download = (bucketName: string, objectName: string) => {
const anchor = document.createElement("a");
document.body.appendChild(anchor);
const token: string = storage.getItem("token")!;
var xhr = new XMLHttpRequest();
const xhr = new XMLHttpRequest();
xhr.open(
"GET",
@@ -155,12 +271,12 @@ class ListObjects extends React.Component<
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
xhr.responseType = "blob";
xhr.onload = function(e) {
if (this.status == 200) {
var blob = new Blob([this.response], {
xhr.onload = function (e) {
if (this.status === 200) {
const blob = new Blob([this.response], {
type: "octet/stream",
});
var blobUrl = window.URL.createObjectURL(blob);
const blobUrl = window.URL.createObjectURL(blob);
anchor.href = blobUrl;
anchor.download = objectName;
@@ -171,115 +287,228 @@ class ListObjects extends React.Component<
}
};
xhr.send();
}
};
bucketFilter(): void {}
const displayParsedDate = (date: string) => {
return <reactMoment.default>{date}</reactMoment.default>;
};
render() {
const { classes } = this.props;
const {
records,
loading,
selectedObject,
selectedBucket,
deleteOpen,
filterObjects,
} = this.state;
const displayParsedDate = (date: string) => {
return <Moment>{date}</Moment>;
};
const confirmDeleteObject = (object: string) => {
setDeleteOpen(true);
setSelectedObject(object);
};
const confirmDeleteObject = (object: string) => {
this.setState({ deleteOpen: true, selectedObject: object });
};
const downloadObject = (object: string) => {
download(selectedBucket, object);
};
const downloadObject = (object: string) => {
this.download(selectedBucket, object);
};
const openPath = (idElement: string) => {
const currentPath = get(match, "url", "/object-browser");
const tableActions = [
{ type: "download", onClick: downloadObject, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteObject, sendOnlyId: true },
];
// Element is a folder, we redirect to it
if (idElement.endsWith("/")) {
const idElementClean = idElement
.substr(0, idElement.length - 1)
.split("/");
const lastIndex = idElementClean.length - 1;
const newPath = `${currentPath}/${idElementClean[lastIndex]}`;
const filteredRecords = records.filter((b: BucketObject) => {
if (filterObjects === "") {
return true;
} else {
if (b.name.indexOf(filterObjects) >= 0) {
return true;
} else {
return false;
}
}
});
addRoute(newPath, idElementClean[lastIndex]);
return;
}
// Element is a file. we open details here
// TODO: Add details open function here.
//console.log("object", idElementClean);
};
const uploadObject = (e: any): void => {
// Handle of deeper routes.
const currentPath = routesList[routesList.length - 1].route;
const splitPaths = currentPath
.split("/")
.filter((item) => item.trim() !== "");
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 (
<React.Fragment>
{deleteOpen && (
<DeleteObject
deleteOpen={deleteOpen}
selectedBucket={selectedBucket}
selectedObject={selectedObject}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
)}
<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,
});
<div className={classes.fileName}>
<div className={icon} />
<span>{splitItem[splitItem.length - 1]}</span>
</div>
);
};
const filteredRecords = records.filter((b: BucketObject) => {
if (filterObjects === "") {
return true;
} else {
if (b.name.indexOf(filterObjects) >= 0) {
return true;
} else {
return false;
}
}
});
return (
<React.Fragment>
{deleteOpen && (
<DeleteObject
deleteOpen={deleteOpen}
selectedBucket={selectedBucket}
selectedObject={selectedObject}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
{createFolderOpen && (
<CreateFolderModal
modalOpen={createFolderOpen}
folderName={routesList[routesList.length - 1].route}
onClose={closeAddFolderModal}
/>
)}
<Snackbar
open={openSnackbar}
message={snackBarMessage}
action={snackBarAction}
/>
<PageHeader label="Object Browser" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.obTitleSection}>
<div>
<BrowserBreadcrumbs />
</div>
<div className={classes.buttonsContainer}>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
component="label"
onClick={() => {
setCreateFolderOpen(true);
}}
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" },
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
},
{
label: "Size",
elementKey: "size",
renderFunction: niceBytes,
},
]}
isLoading={loading}
entityName="Objects"
idField="name"
records={filteredRecords}
/>
</Grid>
>
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>
</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
// 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 ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
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 Typography from "@material-ui/core/Typography";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { Button, LinearProgress } from "@material-ui/core";
import api from "../../../../common/api";
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() {
const { classes, open, actualPolicy } = this.props;
const { classes, open } = this.props;
const { addLoading, addError, accessPolicy } = this.state;
return (
<ModalWrapper

View File

@@ -27,6 +27,7 @@ import {
BucketEvent,
BucketEventList,
BucketInfo,
BucketEncryptionInfo,
BucketList,
BucketReplication,
BucketReplicationDestination,
@@ -34,8 +35,7 @@ import {
BucketReplicationRuleDeleteMarker,
BucketVersioning,
} from "../types";
import { Box, Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import { Button } from "@material-ui/core";
import SetAccessPolicy from "./SetAccessPolicy";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../../../icons";
@@ -46,6 +46,8 @@ import { niceBytes } from "../../../../common/utils";
import AddReplicationModal from "./AddReplicationModal";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../Common/PageHeader/PageHeader";
import Checkbox from "@material-ui/core/Checkbox";
import EnableBucketEncryption from "./EnableBucketEncryption";
const styles = (theme: Theme) =>
createStyles({
@@ -191,6 +193,7 @@ interface IViewBucketState {
rowsPerPage: number;
curTab: number;
addScreenOpen: boolean;
enableEncryptionScreenOpen: boolean;
deleteOpen: boolean;
selectedBucket: string;
selectedEvent: BucketEvent | null;
@@ -199,6 +202,7 @@ interface IViewBucketState {
replicationSet: boolean;
openSetReplication: boolean;
isVersioned: boolean;
encryptionEnabled: boolean;
}
class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
@@ -218,6 +222,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
curTab: 0,
rowsPerPage: 10,
addScreenOpen: false,
enableEncryptionScreenOpen: false,
deleteOpen: false,
selectedBucket: "",
selectedEvent: null,
@@ -226,6 +231,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
replicationSet: false,
openSetReplication: false,
isVersioned: false,
encryptionEnabled: false,
};
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() {
this.setState({ setAccessPolicyScreenOpen: false }, () => {
this.loadInfo();
@@ -343,6 +364,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
this.loadInfo();
this.fetchEvents();
this.fetchBucketsSize();
this.fetchBucketEncryptionInfo();
}
bucketFilter(): void {}
@@ -360,14 +382,15 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
rowsPerPage,
deleteOpen,
addScreenOpen,
enableEncryptionScreenOpen,
selectedEvent,
bucketSize,
loadingSize,
replicationSet,
openSetReplication,
isVersioned,
replicationRules,
curTab,
encryptionEnabled,
} = this.state;
const offset = page * rowsPerPage;
@@ -415,6 +438,23 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
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 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 && (
<SetAccessPolicy
bucketName={bucketName}
@@ -490,6 +540,17 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
</div>
<div>Versioning:</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>
</Paper>
</div>

View File

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

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@@ -13,21 +13,32 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect, createRef, ChangeEvent } from "react";
import React, {
useState,
useEffect,
createRef,
useLayoutEffect,
ChangeEvent,
useRef,
} from "react";
import get from "lodash/get";
import debounce from "lodash/debounce";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import get from "lodash/get";
import InputBoxWrapper from "../InputBoxWrapper/InputBoxWrapper";
import HelpIcon from "@material-ui/icons/Help";
import { InputLabel, Tooltip } from "@material-ui/core";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
import InputBoxWrapper from "../InputBoxWrapper/InputBoxWrapper";
import AddIcon from "../../../../../icons/AddIcon";
interface ICSVMultiSelector {
elements: string;
name: string;
label: string;
tooltip?: string;
commonPlaceholder?: string;
classes: any;
withBorder?: boolean;
onChange: (elements: string) => void;
}
@@ -35,16 +46,13 @@ const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
width: 116,
},
inputContainer: {
inputWithBorder: {
border: "1px solid #EAEAEA",
padding: 15,
height: 150,
overflowY: "auto",
padding: 15,
position: "relative",
border: "1px solid #c4c4c4",
marginTop: 15,
},
labelContainer: {
display: "flex",
@@ -56,7 +64,9 @@ const CSVMultiSelector = ({
name,
label,
tooltip = "",
commonPlaceholder = "",
onChange,
withBorder = false,
classes,
}: ICSVMultiSelector) => {
const [currentElements, setCurrentElements] = useState<string[]>([""]);
@@ -75,29 +85,37 @@ const CSVMultiSelector = ({
setCurrentElements(elementsSplit);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [elements, currentElements]);
// Use effect to send new values to onChange
useEffect(() => {
const elementsString = currentElements
.filter((element) => element.trim() !== "")
.join(",");
onChange(elementsString);
const refScroll = bottomList.current;
if (refScroll) {
refScroll.scrollIntoView(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentElements]);
// We avoid multiple re-renders / hang issue typing too fast
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
debouncedOnChange();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentElements]);
// If the last input is not empty, we add a new one
const addEmptyLine = (elementsUp: string[]) => {
if (elementsUp[elementsUp.length - 1].trim() !== "") {
elementsUp.push("");
const refScroll = bottomList.current;
if (refScroll) {
refScroll.scrollIntoView(false);
}
const cpList = [...elementsUp];
cpList.push("");
setCurrentElements(cpList);
}
return elementsUp;
};
// Onchange function for input box, we get the dataset-index & only update that value in the array
@@ -108,10 +126,18 @@ const CSVMultiSelector = ({
const index = get(e.target, "dataset.index", 0);
updatedElement[index] = e.target.value;
updatedElement = addEmptyLine(updatedElement);
setCurrentElements(updatedElement);
};
// Debounce for On Change
const debouncedOnChange = debounce(() => {
const elementsString = currentElements
.filter((element) => element.trim() !== "")
.join(",");
onChange(elementsString);
}, 500);
const inputs = currentElements.map((element, index) => {
return (
<InputBoxWrapper
@@ -122,6 +148,11 @@ const CSVMultiSelector = ({
onChange={onChangeElement}
index={index}
key={`csv-${name}-${index.toString()}`}
placeholder={commonPlaceholder}
overlayIcon={index === currentElements.length - 1 ? <AddIcon /> : null}
overlayAction={() => {
addEmptyLine(currentElements);
}}
/>
);
});
@@ -139,7 +170,11 @@ const CSVMultiSelector = ({
</div>
)}
</InputLabel>
<Grid item xs={12} className={classes.inputContainer}>
<Grid
item
xs={12}
className={`${withBorder ? classes.inputWithBorder : ""}`}
>
{inputs}
<div ref={bottomList} />
</Grid>
@@ -147,5 +182,4 @@ const CSVMultiSelector = ({
</React.Fragment>
);
};
export default withStyles(styles)(CSVMultiSelector);

View File

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

View File

@@ -0,0 +1,83 @@
// 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 HelpIcon from "@material-ui/icons/Help";
import Grid from "@material-ui/core/Grid";
import { Controlled as CodeMirror } from "react-codemirror2";
import { InputLabel, Tooltip } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { fieldBasic } from "../common/styleLibrary";
import "./ConsoleCodeMirror.css";
require("codemirror/mode/javascript/javascript");
interface ICodeWrapper {
value: string;
label?: string;
tooltip?: string;
classes: any;
onChange?: (editor: any, data: any, value: string) => any;
onBeforeChange: (editor: any, data: any, value: string) => any;
readOnly?: boolean;
}
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
});
const CodeMirrorWrapper = ({
value,
label = "",
tooltip = "",
classes,
onChange = () => {},
onBeforeChange,
readOnly = false,
}: ICodeWrapper) => {
return (
<React.Fragment>
<InputLabel className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<CodeMirror
value={value}
options={{
mode: "javascript",
lineNumbers: true,
readOnly,
}}
onBeforeChange={onBeforeChange}
onChange={onChange}
/>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(CodeMirrorWrapper);

View File

@@ -0,0 +1,353 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: #fff;
background: #081C42;
direction: ltr;
font-size: 13px;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: rgba(255,255,255,0.8); /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #ffffff80;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
white-space: nowrap;
color: #000;
font-size: 10px;
height: 18px;
line-height: 18px;
text-align: center;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid white;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-fat-cursor-mark {
background-color: rgba(20, 255, 20, 0.5);
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: 0;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: #fff;}
.cm-s-default .cm-quote {color: #fff;}
.cm-negative {color: #fff;}
.cm-positive {color: #fff;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #fff;}
.cm-s-default .cm-atom {color: #fff;}
.cm-s-default .cm-number {color: #fff;}
.cm-s-default .cm-def {color: #fff;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #fff;}
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #fff;}
.cm-s-default .cm-comment {color: #fff;}
.cm-s-default .cm-string {color: #fff;}
.cm-s-default .cm-string-2 {color: #fff;}
.cm-s-default .cm-meta {color: #fff;}
.cm-s-default .cm-qualifier {color: #fff;}
.cm-s-default .cm-builtin {color: #fff;}
.cm-s-default .cm-bracket {color: #fff;}
.cm-s-default .cm-tag {color: #fff;}
.cm-s-default .cm-attribute {color: #fff;}
.cm-s-default .cm-hr {color: #fff;}
.cm-s-default .cm-link {color: #fff;}
.cm-s-default .cm-error {color: #fff;}
.cm-invalidchar {color: #fff;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #fff;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #fff;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 50px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -50px; margin-right: -50px;
padding-bottom: 50px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 50px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -50px;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre.CodeMirror-line,
.CodeMirror-wrap pre.CodeMirror-line-like {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
padding: 0.1px; /* Force widget margins to stay inside of the container */
}
.CodeMirror-widget {}
.CodeMirror-rtl pre { direction: rtl; }
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background-color: #ffa;
background-color: rgba(255, 255, 0, .4);
}
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }

View File

@@ -0,0 +1,141 @@
// 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, InputLabel, TextField, Tooltip } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface CommentBoxProps {
label: string;
classes: any;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
value: string | boolean;
id: string;
name: string;
disabled?: boolean;
tooltip?: string;
index?: number;
error?: string;
required?: boolean;
placeholder?: string;
}
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
marginBottom: 16,
fontSize: 14,
},
textBoxContainer: {
flexGrow: 1,
position: "relative",
},
errorState: {
color: "#b53b4b",
fontSize: 14,
position: "absolute",
top: 7,
right: 7,
},
cssOutlinedInput: {
borderColor: "#9C9C9C",
padding: 16,
},
rootContainer: {
"& .MuiOutlinedInput-inputMultiline": {
...fieldBasic.inputLabel,
fontSize: 13,
minHeight: 150,
},
},
});
const CommentBoxWrapper = ({
label,
onChange,
value,
id,
name,
disabled = false,
tooltip = "",
index = 0,
error = "",
required = false,
placeholder = "",
classes,
}: CommentBoxProps) => {
let inputProps: any = { "data-index": index };
return (
<React.Fragment>
<Grid
item
xs={12}
className={`${classes.fieldContainer} ${
error !== "" ? classes.errorInField : ""
}`}
>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>
{label}
{required ? "*" : ""}
</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
)}
<div className={classes.textBoxContainer}>
<TextField
id={id}
name={name}
fullWidth
value={value}
disabled={disabled}
onChange={onChange}
multiline
inputProps={inputProps}
error={error !== ""}
helperText={error}
placeholder={placeholder}
InputLabelProps={{
shrink: true,
}}
InputProps={{
classes: {
notchedOutline: classes.cssOutlinedInput,
root: classes.rootContainer,
},
}}
variant="outlined"
/>
</div>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(CommentBoxWrapper);

View File

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

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { InputLabel, Switch, Tooltip } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
@@ -22,7 +22,7 @@ import { actionsTray, fieldBasic } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface IFormSwitch {
label: string;
label?: string;
classes: any;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
value: string | boolean;
@@ -31,7 +31,9 @@ interface IFormSwitch {
disabled?: boolean;
tooltip?: string;
index?: number;
indicatorLabel?: string;
checked: boolean;
switchOnly?: boolean;
}
const styles = (theme: Theme) =>
@@ -82,7 +84,7 @@ const styles = (theme: Theme) =>
},
actionsTitle: {
fontWeight: 600,
color: "#000",
color: "#081C42",
fontSize: 16,
alignSelf: "center",
},
@@ -95,7 +97,7 @@ const styles = (theme: Theme) =>
"& .input": {
"&::placeholder": {
fontWeight: 600,
color: "#000",
color: "#081C42",
},
},
},
@@ -107,6 +109,15 @@ const styles = (theme: Theme) =>
paddingBottom: 14,
marginBottom: 20,
},
indicatorLabel: {
fontSize: 12,
fontWeight: 600,
color: "#081C42",
margin: "0 8px 0 10px",
},
switchContainer: {
display: "flex",
},
...actionsTray,
...fieldBasic,
});
@@ -127,7 +138,7 @@ const StyledSwitch = withStyles({
color: "#fff",
},
"&$checked + $track": {
backgroundColor: "#000",
backgroundColor: "#081C42",
opacity: 1,
height: 15,
},
@@ -135,14 +146,14 @@ const StyledSwitch = withStyles({
checked: {},
track: {
height: 15,
backgroundColor: "#000",
backgroundColor: "#081C42",
opacity: 1,
padding: 0,
marginTop: 1.5,
},
thumb: {
backgroundColor: "#fff",
border: "#000 1px solid",
border: "#081C42 1px solid",
boxShadow: "none",
width: 18,
height: 18,
@@ -152,16 +163,44 @@ const StyledSwitch = withStyles({
})(Switch);
const FormSwitchWrapper = ({
label,
label = "",
onChange,
value,
id,
name,
checked = false,
disabled = false,
switchOnly = false,
tooltip = "",
indicatorLabel = "",
classes,
}: IFormSwitch) => {
const switchComponent = (
<React.Fragment>
<div className={classes.switchContainer}>
<StyledSwitch
checked={checked}
onChange={onChange}
color="primary"
name={name}
inputProps={{ "aria-label": "primary checkbox" }}
disabled={disabled}
disableRipple
disableFocusRipple
disableTouchRipple
value={value}
/>
{indicatorLabel !== "" && (
<span className={classes.indicatorLabel}>{indicatorLabel}</span>
)}
</div>
</React.Fragment>
);
if (switchOnly) {
return switchComponent;
}
return (
<React.Fragment>
<Grid item xs={12} className={`${classes.wrapperContainer}`}>
@@ -177,21 +216,7 @@ const FormSwitchWrapper = ({
)}
</InputLabel>
)}
<div className={classes.textBoxContainer}>
<StyledSwitch
checked={checked}
onChange={onChange}
color="primary"
name={name}
inputProps={{ "aria-label": "primary checkbox" }}
disabled={disabled}
disableRipple
disableFocusRipple
disableTouchRipple
value={value}
/>
</div>
{switchComponent}
</Grid>
</React.Fragment>
);

View File

@@ -16,6 +16,7 @@
import React from "react";
import {
Grid,
IconButton,
InputLabel,
TextField,
TextFieldProps,
@@ -49,6 +50,8 @@ interface InputBoxProps {
placeholder?: string;
min?: string;
max?: string;
overlayIcon?: any;
overlayAction?: () => void;
}
const styles = (theme: Theme) =>
@@ -57,7 +60,10 @@ const styles = (theme: Theme) =>
...tooltipHelper,
textBoxContainer: {
flexGrow: 1,
},
textBoxWithIcon: {
position: "relative",
paddingRight: 25,
},
errorState: {
color: "#b53b4b",
@@ -66,6 +72,15 @@ const styles = (theme: Theme) =>
top: 7,
right: 7,
},
overlayAction: {
position: "absolute",
right: 0,
top: 15,
"& svg": {
maxWidth: 15,
maxHeight: 15,
},
},
});
const inputStyles = makeStyles((theme: Theme) =>
@@ -76,15 +91,24 @@ const inputStyles = makeStyles((theme: Theme) =>
borderColor: "#9c9c9c",
},
},
disabled: {
"&.MuiInput-underline::before": {
borderColor: "#eaeaea",
borderBottomStyle: "solid",
},
},
input: {
padding: "15px 5px 10px",
padding: "15px 30px 10px 5px",
color: "#393939",
fontSize: 13,
fontWeight: 600,
"&:placeholder": {
color: "#393939",
opacity: 1,
},
},
error: {
color: "#b53b4b",
boxShadow: "inset 0px 0px 1px 1px #b53b4b",
},
})
);
@@ -117,6 +141,8 @@ const InputBoxWrapper = ({
placeholder = "",
min,
max,
overlayIcon = null,
overlayAction,
classes,
}: InputBoxProps) => {
let inputProps: any = { "data-index": index };
@@ -139,12 +165,7 @@ const InputBoxWrapper = ({
}`}
>
{label !== "" && (
<InputLabel
htmlFor={id}
className={`${error !== "" ? classes.fieldLabelError : ""} ${
classes.inputLabel
}`}
>
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>
{label}
{required ? "*" : ""}
@@ -174,12 +195,28 @@ const InputBoxWrapper = ({
error={error !== ""}
helperText={error}
placeholder={placeholder}
InputLabelProps={{
shrink: true,
}}
className={classes.inputRebase}
/>
</div>
{overlayIcon && (
<div className={classes.overlayAction}>
<IconButton
onClick={
overlayAction
? () => {
overlayAction();
}
: () => null
}
size={"small"}
disableFocusRipple={false}
disableRipple={false}
disableTouchRipple={false}
>
{overlayIcon}
</IconButton>
</div>
)}
</Grid>
</React.Fragment>
);

View File

@@ -25,7 +25,7 @@ import {
withStyles,
makeStyles,
} from "@material-ui/core/styles";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import { fieldBasic, radioIcons, tooltipHelper } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
export interface SelectorTypes {
@@ -49,8 +49,20 @@ const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
radioBoxContainer: {
flexGrow: 1,
radioBoxContainer: {},
fieldContainer: {
...fieldBasic.fieldContainer,
display: "flex",
justifyContent: "space-between",
borderBottom: "#9c9c9c 1px solid",
paddingBottom: 10,
marginTop: 11,
},
checkedOption: {
"& .MuiFormControlLabel-label": {
color: "#000",
fontWeight: 700,
},
},
});
@@ -60,31 +72,7 @@ const radioStyles = makeStyles({
backgroundColor: "transparent",
},
},
icon: {
borderRadius: "100%",
width: 14,
height: 14,
border: "1px solid #000",
},
checkedIcon: {
borderRadius: "100%",
width: 14,
height: 14,
border: "1px solid #000",
padding: 4,
position: "relative",
"&::after": {
content: '" "',
width: 8,
height: 8,
borderRadius: "100%",
display: "block",
position: "absolute",
backgroundColor: "#000",
top: 2,
left: 2,
},
},
...radioIcons,
});
const RadioButton = (props: RadioProps) => {
@@ -95,8 +83,8 @@ const RadioButton = (props: RadioProps) => {
className={classes.root}
disableRipple
color="default"
checkedIcon={<span className={classes.checkedIcon} />}
icon={<span className={classes.icon} />}
checkedIcon={<span className={classes.radioSelectedIcon} />}
icon={<span className={classes.radioUnselectedIcon} />}
{...props}
/>
);
@@ -143,6 +131,11 @@ export const RadioGroupSelector = ({
value={selectorOption.value}
control={<RadioButton />}
label={selectorOption.label}
className={
selectorOption.value === currentSelection
? classes.checkedOption
: ""
}
/>
);
})}

View File

@@ -20,7 +20,6 @@ export const fieldBasic = {
inputLabel: {
fontWeight: 600,
marginRight: 10,
width: 160,
fontSize: 15,
color: "#000",
textAlign: "left" as const,
@@ -36,6 +35,7 @@ export const fieldBasic = {
},
fieldContainer: {
marginBottom: 20,
position: "relative" as const,
},
tooltipContainer: {
marginLeft: 5,
@@ -53,6 +53,30 @@ export const modalBasic = {
formSlider: {
marginLeft: 0,
},
clearButton: {
border: "0",
backgroundColor: "transparent",
color: "#393939",
fontWeight: 600,
fontSize: 14,
marginRight: 10,
outline: "0",
padding: "16px 25px 16px 25px",
cursor: "pointer",
},
floatingEnabled: {
position: "absolute" as const,
right: 58,
zIndex: 1000,
marginTop: -38,
},
configureString: {
border: "#EAEAEA 1px solid",
borderRadius: 4,
padding: "24px 50px",
overflowY: "auto" as const,
height: 170,
},
};
export const tooltipHelper = {
@@ -76,6 +100,21 @@ export const checkboxIcons = {
},
};
const radioBasic = {
width: 12,
height: 12,
borderRadius: "100%",
};
export const radioIcons = {
radioUnselectedIcon: { ...radioBasic, border: "1px solid #000" },
radioSelectedIcon: {
...radioBasic,
border: "1px solid #000",
backgroundColor: "#000",
},
};
export const containerForHeader = (bottomSpacing: any) => ({
container: {
padding: "110px 33px 30px",
@@ -106,7 +145,7 @@ export const actionsTray = {
export const searchField = {
searchField: {
flexGrow: 1,
marginRight: 30,
height: 40,
background: "#FFFFFF",
borderRadius: 5,
border: "#EAEDEE 1px solid",
@@ -137,5 +176,34 @@ export const predefinedList = {
color: "#393939",
fontSize: 12,
fontWeight: 600,
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,
marginRight: 10,
"& ul": {
padding: 15,
padding: "0 15px 0 0",
"& li": {
listStyle: "lower-roman",

View File

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

View File

@@ -16,12 +16,7 @@
import React from "react";
import { Dialog, DialogContent, DialogTitle } from "@material-ui/core";
import IconButton from "@material-ui/core/IconButton";
import {
createStyles,
makeStyles,
Theme,
withStyles,
} from "@material-ui/core/styles";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
interface IModalProps {
classes: any;
@@ -29,11 +24,12 @@ interface IModalProps {
modalOpen: boolean;
title: string;
children: any;
wideLimit?: boolean;
}
const baseCloseLine = {
content: '" "',
borderLeft: "2px solid #707070",
borderLeft: "2px solid #9C9C9C",
height: 33,
width: 1,
position: "absolute",
@@ -61,10 +57,10 @@ const styles = (theme: Theme) =>
},
modalCloseIcon: {
fontSize: 35,
color: "#707070",
color: "#9C9C9C",
fontWeight: 300,
"&:hover": {
color: "#000",
color: "#9C9C9C",
},
},
closeIcon: {
@@ -77,7 +73,7 @@ const styles = (theme: Theme) =>
transform: "rotate(-45deg)",
},
"&:hover::before, &:hover::after": {
borderColor: "#000",
borderColor: "#9C9C9C",
},
width: 24,
height: 24,
@@ -95,6 +91,10 @@ const styles = (theme: Theme) =>
modalContent: {
padding: "0 50px",
},
customDialogSize: {
width: "100%",
maxWidth: 765,
},
});
const ModalWrapper = ({
@@ -103,15 +103,22 @@ const ModalWrapper = ({
title,
children,
classes,
wideLimit = true,
}: IModalProps) => {
const customSize = wideLimit
? {
classes: {
paper: classes.customDialogSize,
},
}
: { maxWidth: "md" as const, fullWidth: true };
return (
<Dialog
open={modalOpen}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"md"}
fullWidth
{...customSize}
>
<div className={classes.dialogContainer}>
<div className={classes.closeContainer}>

View File

@@ -7,13 +7,17 @@ const DeleteIcon = ({ active = false }: IIcon) => {
xmlns="http://www.w3.org/2000/svg"
width="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
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"
transform="translate(1225 657)"
d="M6.757,29.959v-1H3.636v1H0v1H10.4v-1Z"
/>
<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>
</svg>

View File

@@ -33,7 +33,10 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { TablePaginationActionsProps } from "@material-ui/core/TablePagination/TablePaginationActions";
import TableActionButton from "./TableActionButton";
import history from "../../../../history";
import { checkboxIcons } from "../FormComponents/common/styleLibrary";
import {
checkboxIcons,
radioIcons,
} from "../FormComponents/common/styleLibrary";
//Interfaces for table Items
@@ -49,7 +52,9 @@ interface IColumns {
elementKey: string;
sortable?: boolean;
renderFunction?: (input: any) => any;
renderFullObject?: boolean;
globalClass?: any;
rowClass?: any;
}
interface IPaginatorConfig {
@@ -80,15 +85,23 @@ interface TableWrapperProps {
entityName: string;
selectedItems?: string[];
stickyHeader?: boolean;
radioSelection?: boolean;
customEmptyMessage?: string;
paginatorConfig?: IPaginatorConfig;
}
const borderColor = "#eaeaea";
const borderColor = "#9c9c9c80";
const rowText = {
fontWeight: 400,
fontSize: 14,
borderColor: borderColor,
borderWidth: "0.5px",
height: 40,
transitionDuration: "0.3s",
padding: "initial",
paddingRight: 6,
paddingLeft: 6,
};
const styles = (theme: Theme) =>
@@ -101,9 +114,18 @@ const styles = (theme: Theme) =>
overflow: "auto",
flexDirection: "column",
padding: "19px 38px",
minHeight: "200px",
boxShadow: "none",
border: "#e7e7e7 1px solid",
border: "#EAEDEE 1px solid",
borderRadius: 3,
minHeight: "calc(100vh - 340px)",
},
allTableSettings: {
"& .MuiTableCell-sizeSmall:last-child": {
paddingRight: "initial",
},
"& .MuiTableCell-body.MuiTableCell-sizeSmall:last-child": {
paddingRight: 6,
},
},
minTableHeader: {
color: "#393939",
@@ -111,13 +133,15 @@ const styles = (theme: Theme) =>
"& th": {
fontWeight: 700,
fontSize: 14,
paddingBottom: 15,
borderColor: borderColor,
borderColor: "#39393980",
borderWidth: "0.5px",
padding: "6px 0 10px",
},
},
},
rowUnselected: {
...rowText,
color: "#393939",
},
rowSelected: {
...rowText,
@@ -130,8 +154,12 @@ const styles = (theme: Theme) =>
padding: "5px 38px",
},
checkBoxHeader: {
width: 50,
textAlign: "left",
paddingRight: 10,
"&.MuiTableCell-paddingCheckbox": {
paddingBottom: 9,
paddingBottom: 4,
paddingLeft: 0,
},
},
actionsContainer: {
@@ -143,6 +171,7 @@ const styles = (theme: Theme) =>
},
checkBoxRow: {
borderColor: borderColor,
padding: "0 10px 0 0",
},
loadingBox: {
paddingTop: "100px",
@@ -153,12 +182,17 @@ const styles = (theme: Theme) =>
"&:hover": {
backgroundColor: "#ececec",
"& td": {
fontWeight: 600,
},
},
},
rowClickable: {
cursor: "pointer",
},
...checkboxIcons,
...radioIcons,
});
// Function that renders Title Columns
@@ -186,13 +220,17 @@ const rowColumnsMap = (
const itemElement = isString(itemData)
? itemData
: get(itemData, column.elementKey, null); // If the element is just a string, we render it as it is
const renderConst = column.renderFullObject ? itemData : itemElement;
const renderElement = column.renderFunction
? column.renderFunction(itemElement)
: itemElement; // If render function is set, we send the value to the function.
? column.renderFunction(renderConst)
: renderConst; // If render function is set, we send the value to the function.
return (
<TableCell
key={`tbRE-${column.elementKey}-${index}`}
className={isSelected ? classes.rowSelected : classes.rowUnselected}
className={`${column.rowClass} ${
isSelected ? classes.rowSelected : classes.rowUnselected
}`}
>
{renderElement}
</TableCell>
@@ -239,6 +277,8 @@ const TableWrapper = ({
idField,
classes,
stickyHeader = false,
radioSelection = false,
customEmptyMessage = "",
paginatorConfig,
}: TableWrapperProps) => {
const findView = itemActions
@@ -273,15 +313,15 @@ const TableWrapper = ({
</Grid>
)}
{records && !isLoading && records.length > 0 ? (
<Table size="small" stickyHeader={stickyHeader}>
<Table
size="small"
stickyHeader={stickyHeader}
className={classes.allTableSettings}
>
<TableHead className={classes.minTableHeader}>
<TableRow>
{onSelect && selectedItems && (
<TableCell
padding="checkbox"
align="center"
className={classes.checkBoxHeader}
>
<TableCell align="center" className={classes.checkBoxHeader}>
Select
</TableCell>
)}
@@ -312,17 +352,13 @@ const TableWrapper = ({
key={`tb-${entityName}-${index.toString()}`}
className={`${findView ? classes.rowClickable : ""} ${
classes.rowElement
}`}
} rowElementRaw`}
onClick={() => {
clickAction(record);
}}
>
{onSelect && selectedItems && (
<TableCell
padding="checkbox"
align="center"
className={classes.checkBoxRow}
>
<TableCell align="center" className={classes.checkBoxRow}>
<Checkbox
value={isString(record) ? record : record[idField]}
color="primary"
@@ -333,8 +369,24 @@ const TableWrapper = ({
e.stopPropagation();
e.preventDefault();
}}
checkedIcon={<span className={classes.checkedIcon} />}
icon={<span className={classes.unCheckedIcon} />}
checkedIcon={
<span
className={
radioSelection
? classes.radioSelectedIcon
: classes.checkedIcon
}
/>
}
icon={
<span
className={
radioSelection
? classes.radioUnselectedIcon
: classes.unCheckedIcon
}
/>
}
/>
</TableCell>
)}
@@ -362,7 +414,13 @@ const TableWrapper = ({
</Table>
) : (
<React.Fragment>
{!isLoading && <div>{`There are no ${entityName} yet.`}</div>}
{!isLoading && (
<div>
{customEmptyMessage !== ""
? customEmptyMessage
: `There are no ${entityName} yet.`}
</div>
)}
</React.Fragment>
)}
</Paper>

View File

@@ -20,8 +20,9 @@ import Grid from "@material-ui/core/Grid";
import { IElementValue, KVField } from "./types";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import CSVMultiSelector from "../Common/FormComponents/CSVMultiSelector/CSVMultiSelector";
import CommentBoxWrapper from "../Common/FormComponents/CommentBoxWrapper/CommentBoxWrapper";
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
interface IConfGenericProps {
onChange: (newValue: IElementValue[]) => void;
@@ -77,6 +78,7 @@ const ConfTargetGeneric = ({
});
setValueHolder(values);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fields, defaultVals]);
useEffect(() => {
@@ -94,20 +96,21 @@ const ConfTargetGeneric = ({
const fieldDefinition = (field: KVField, item: number) => {
switch (field.type) {
case "on|off":
const value = valueHolder[item] ? valueHolder[item].value : "false";
return (
<RadioGroupSelector
currentSelection={valueHolder[item] ? valueHolder[item].value : ""}
<FormSwitchWrapper
indicatorLabel="On"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.checked ? "true" : "false";
setValueElement(field.name, value, item);
}}
id={field.name}
name={field.name}
label={field.label}
value={"switch_on"}
tooltip={field.tooltip}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValueElement(field.name, e.target.value, item)
}
selectorOptions={[
{ label: "On", value: "true" },
{ label: "Off", value: "false" },
]}
checked={value === "true"}
/>
);
case "csv":
@@ -120,6 +123,22 @@ const ConfTargetGeneric = ({
setValueElement(field.name, value, item)
}
tooltip={field.tooltip}
commonPlaceholder={field.placeholder}
withBorder={!!field.withBorder}
/>
);
case "comment":
return (
<CommentBoxWrapper
id={field.name}
name={field.name}
label={field.label}
tooltip={field.tooltip}
value={valueHolder[item] ? valueHolder[item].value : ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValueElement(field.name, e.target.value, item)
}
placeholder={field.placeholder}
/>
);
default:
@@ -134,6 +153,7 @@ const ConfTargetGeneric = ({
setValueElement(field.name, e.target.value, item)
}
multiline={!!field.multiline}
placeholder={field.placeholder}
/>
);
}

View File

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

View File

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

View File

@@ -16,12 +16,16 @@
import React, { useCallback, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { FormControlLabel, Switch } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import { IElementValue } from "../types";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import {
modalBasic,
predefinedList,
} from "../../Common/FormComponents/common/styleLibrary";
import CommentBoxWrapper from "../../Common/FormComponents/CommentBoxWrapper/CommentBoxWrapper";
import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
interface IConfMySqlProps {
onChange: (newValue: IElementValue[]) => void;
@@ -31,6 +35,7 @@ interface IConfMySqlProps {
const styles = (theme: Theme) =>
createStyles({
...modalBasic,
...predefinedList,
});
const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
@@ -104,44 +109,41 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
setDsnString(cs);
}, [user, dbName, password, port, host, setDsnString, configToDsnString]);
const switcherChangeEvt = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
// build dsn_string
const cs = configToDsnString();
setDsnString(cs);
} else {
// parse dsn_string
const kv = parseDsnString(dsnString, [
"host",
"port",
"dbname",
"user",
"password",
]);
setHostname(kv.get("host") ? kv.get("host") + "" : "");
setPort(kv.get("port") ? kv.get("port") + "" : "");
setDbName(kv.get("dbname") ? kv.get("dbname") + "" : "");
setUser(kv.get("user") ? kv.get("user") + "" : "");
setPassword(kv.get("password") ? kv.get("password") + "" : "");
}
setUseDsnString(event.target.checked);
};
return (
<Grid container className={classes.formScrollable}>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch
checked={useDsnString}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
// build dsn_string
const cs = configToDsnString();
setDsnString(cs);
} else {
// parse dsn_string
const kv = parseDsnString(dsnString, [
"host",
"port",
"dbname",
"user",
"password",
]);
setHostname(kv.get("host") ? kv.get("host") + "" : "");
setPort(kv.get("port") ? kv.get("port") + "" : "");
setDbName(kv.get("dbname") ? kv.get("dbname") + "" : "");
setUser(kv.get("user") ? kv.get("user") + "" : "");
setPassword(
kv.get("password") ? kv.get("password") + "" : ""
);
}
setUseDsnString(event.target.checked);
}}
name="checkedB"
color="primary"
/>
}
label="Enter DSN String"
className={classes.formSlider}
<FormSwitchWrapper
label={"Enter DNS String"}
checked={useDsnString}
id="checkedB"
name="checkedB"
onChange={switcherChangeEvt}
value={"dnsString"}
indicatorLabel={"On"}
/>
</Grid>
{useDsnString ? (
@@ -160,62 +162,78 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="host"
name="host"
label="Host"
value={host}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setHostname(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="db-name"
name="db-name"
label="DB Name"
value={dbName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDbName(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="port"
name="port"
label="Port"
value={port}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPort(e.target.value);
}}
/>
</Grid>
<Grid item xs={12} className={classes.configureString}>
<Grid item xs={12}>
<InputBoxWrapper
id="host"
name="host"
label=""
placeholder="Enter Host"
value={host}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setHostname(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="db-name"
name="db-name"
label=""
placeholder="Enter DB Name"
value={dbName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDbName(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="port"
name="port"
label=""
placeholder="Enter Port"
value={port}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPort(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="user"
name="user"
label="User"
value={user}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser(e.target.value);
}}
/>
<Grid item xs={12}>
<InputBoxWrapper
id="user"
name="user"
label=""
placeholder="Enter User"
value={user}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="password"
name="password"
label=""
placeholder="Enter Password"
type="password"
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
}}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.predefinedTitle}>
Connection String
</Grid>
<Grid item xs={12} className={classes.predefinedList}>
{dsnString}
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="password"
name="password"
label="Password"
type="password"
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
}}
/>
<br />
</Grid>
</React.Fragment>
)}
@@ -224,6 +242,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
id="table"
name="table"
label="Table"
placeholder="Enter Table Name"
value={table}
tooltip="DB table name to store/update events, table is auto-created"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -252,6 +271,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
id="queue-dir"
name="queue_dir"
label="Queue Dir"
placeholder="Enter Queue Dir"
value={queueDir}
tooltip="staging dir for undelivered messages e.g. '/home/events'"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -264,6 +284,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
id="queue-limit"
name="queue_limit"
label="Queue Limit"
placeholder="Enter Queue Limit"
type="number"
value={queueLimit}
tooltip="maximum limit for undelivered messages, defaults to '10000'"
@@ -273,11 +294,11 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
<CommentBoxWrapper
id="comment"
name="comment"
label="Comment"
multiline={true}
placeholder="Enter Comment"
value={comment}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setComment(e.target.value);

View File

@@ -16,13 +16,17 @@
import React, { useCallback, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { FormControlLabel, Switch } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { IElementValue } from "../types";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import {
modalBasic,
predefinedList,
} from "../../Common/FormComponents/common/styleLibrary";
import CommentBoxWrapper from "../../Common/FormComponents/CommentBoxWrapper/CommentBoxWrapper";
import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
interface IConfPostgresProps {
onChange: (newValue: IElementValue[]) => void;
@@ -32,6 +36,7 @@ interface IConfPostgresProps {
const styles = (theme: Theme) =>
createStyles({
...modalBasic,
...predefinedList,
});
const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
@@ -45,7 +50,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
const [port, setPort] = useState<string>("");
const [user, setUser] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [sslMode, setSslMode] = useState<string>("require");
const [sslMode, setSslMode] = useState<string>(" ");
const [table, setTable] = useState<string>("");
const [format, setFormat] = useState<string>("namespace");
@@ -126,8 +131,11 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
if (port !== "") {
strValue = `${strValue} port=${port}`;
}
if (sslMode !== " ") {
strValue = `${strValue} sslmode=${sslMode}`;
}
strValue = `${strValue} sslmode=${sslMode}`;
strValue = `${strValue} `;
return strValue.trim();
}, [host, dbName, user, password, port, sslMode]);
@@ -169,48 +177,46 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
configToString,
]);
useEffect(() => {
if (useConnectionString) {
// build connection_string
const cs = configToString();
setConnectionString(cs);
return;
}
// parse connection_string
const kv = parseConnectionString(connectionString, [
"host",
"port",
"dbname",
"user",
"password",
"sslmode",
]);
setHostname(kv.get("host") ? kv.get("host") + "" : "");
setPort(kv.get("port") ? kv.get("port") + "" : "");
setDbName(kv.get("dbname") ? kv.get("dbname") + "" : "");
setUser(kv.get("user") ? kv.get("user") + "" : "");
setPassword(kv.get("password") ? kv.get("password") + "" : "");
setSslMode(kv.get("sslmode") ? kv.get("sslmode") + "" : " ");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [useConnectionString]);
return (
<Grid container className={classes.formScrollable}>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch
checked={useConnectionString}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
// build connection_string
const cs = configToString();
setConnectionString(cs);
} else {
// parse connection_string
const kv = parseConnectionString(connectionString, [
"host",
"port",
"dbname",
"user",
"password",
"sslmode",
]);
setHostname(kv.get("host") ? kv.get("host") + "" : "");
setPort(kv.get("port") ? kv.get("port") + "" : "");
setDbName(kv.get("dbname") ? kv.get("dbname") + "" : "");
setUser(kv.get("user") ? kv.get("user") + "" : "");
setPassword(
kv.get("password") ? kv.get("password") + "" : ""
);
setSslMode(
kv.get("sslmode") ? kv.get("sslmode") + "" : "require"
);
}
setUseConnectionString(event.target.checked);
}}
name="checkedB"
color="primary"
/>
}
label="Enter Connection String"
className={classes.formSlider}
<FormSwitchWrapper
label={"Manually Configure String"}
checked={useConnectionString}
id="manualString"
name="manualString"
onChange={(e) => {
setUseConnectionString(e.target.checked);
}}
value={"manualString"}
indicatorLabel={"On"}
/>
</Grid>
{useConnectionString ? (
@@ -229,81 +235,97 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="host"
name="host"
label="Host"
value={host}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setHostname(e.target.value);
}}
/>
<Grid item xs={12} className={classes.configureString}>
<Grid item xs={12}>
<InputBoxWrapper
id="host"
name="host"
label=""
placeholder="Enter Host"
value={host}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setHostname(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="db-name"
name="db-name"
label=""
placeholder="Enter DB Name"
value={dbName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDbName(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="port"
name="port"
label=""
placeholder="Enter Port"
value={port}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPort(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
value={sslMode}
label=""
id="sslmode"
name="sslmode"
onChange={(e): void => {
if (e.target.value !== undefined) {
setSslMode(e.target.value + "");
}
}}
options={[
{ label: "Enter SSL Mode", value: " " },
{ label: "Require", value: "require" },
{ label: "Disable", value: "disable" },
{ label: "Verify CA", value: "verify-ca" },
{ label: "Verify Full", value: "verify-full" },
]}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="user"
name="user"
label=""
placeholder="Enter User"
value={user}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="password"
name="password"
label=""
type="password"
placeholder="Enter Password"
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
}}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.predefinedTitle}>
Connection String
</Grid>
<Grid item xs={12} className={classes.predefinedList}>
{connectionString}
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="db-name"
name="db-name"
label="DB Name"
value={dbName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDbName(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="port"
name="port"
label="Port"
value={port}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPort(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
value={sslMode}
label="SSL Mode"
id="sslmode"
name="sslmode"
onChange={(e): void => {
if (e.target.value !== undefined) {
setSslMode(e.target.value + "");
}
}}
options={[
{ label: "Require", value: "require" },
{ label: "Disable", value: "disable" },
{ label: "Verify CA", value: "verify-ca" },
{ label: "Verify Full", value: "verify-full" },
]}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="user"
name="user"
label="User"
value={user}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="password"
name="password"
label="Password"
type="password"
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
}}
/>
<br />
</Grid>
</React.Fragment>
)}
@@ -312,6 +334,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
id="table"
name="table"
label="Table"
placeholder={"Enter Table Name"}
value={table}
tooltip="DB table name to store/update events, table is auto-created"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -340,6 +363,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
id="queue-dir"
name="queue_dir"
label="Queue Dir"
placeholder="Enter Queue Directory"
value={queueDir}
tooltip="staging dir for undelivered messages e.g. '/home/events'"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -352,6 +376,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
id="queue-limit"
name="queue_limit"
label="Queue Limit"
placeholder="Enter Queue Limit"
type="number"
value={queueLimit}
tooltip="maximum limit for undelivered messages, defaults to '10000'"
@@ -361,11 +386,11 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
<CommentBoxWrapper
id="comment"
name="comment"
label="Comment"
multiline={true}
placeholder="Enter Comment"
value={comment}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setComment(e.target.value);

View File

@@ -27,7 +27,9 @@ export type KVFieldType =
| "duration"
| "uri"
| "sentence"
| "csv";
| "csv"
| "comment"
| "switch";
export interface KVField {
name: string;
@@ -37,6 +39,8 @@ export interface KVField {
type: KVFieldType;
options?: SelectorTypes[];
multiline?: boolean;
placeholder?: string;
withBorder?: boolean;
}
export interface IConfigurationElement {

File diff suppressed because it is too large Load Diff

View File

@@ -16,27 +16,12 @@
import React, { useEffect } from "react";
import clsx from "clsx";
import {
createStyles,
StyledProps,
Theme,
withStyles,
} from "@material-ui/core/styles";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import CssBaseline from "@material-ui/core/CssBaseline";
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 Link from "@material-ui/core/Link";
import history from "../../history";
import {
Redirect,
Route,
RouteComponentProps,
Router,
Switch,
withRouter,
} from "react-router-dom";
import { Redirect, Route, Router, Switch } from "react-router-dom";
import { connect } from "react-redux";
import { AppState } from "../../store";
import {
@@ -44,7 +29,6 @@ import {
serverNeedsRestart,
setMenuOpen,
} from "../../actions";
import { ThemedComponentProps } from "@material-ui/core/styles/withTheme";
import Buckets from "./Buckets/Buckets";
import Policies from "./Policies/Policies";
import Permissions from "./Permissions/Permissions";
@@ -58,8 +42,6 @@ import ListNotificationEndpoints from "./NotificationEndopoints/ListNotification
import ConfigurationsList from "./Configurations/ConfigurationPanels/ConfigurationsList";
import { Button, LinearProgress } from "@material-ui/core";
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
import Trace from "./Trace/Trace";
import Logs from "./Logs/Logs";
import Heal from "./Heal/Heal";
import Watch from "./Watch/Watch";
import ListTenants from "./Tenants/ListTenants/ListTenants";
@@ -71,19 +53,6 @@ import ObjectBrowser from "./ObjectBrowser/ObjectBrowser";
import ListObjects from "./Buckets/ListBuckets/Objects/ListObjects/ListObjects";
import License from "./License/License";
function Copyright() {
return (
<Typography variant="body2" color="textSecondary" align="center">
{"Copyright © "}
<Link color="inherit" href="https://material-ui.com/">
MinIO
</Link>{" "}
{new Date().getFullYear()}
{"."}
</Typography>
);
}
const drawerWidth = 245;
const styles = (theme: Theme) =>
@@ -160,6 +129,8 @@ const styles = (theme: Theme) =>
container: {
paddingBottom: theme.spacing(4),
margin: 0,
width: "100%",
maxWidth: "initial",
},
paper: {
padding: theme.spacing(2),
@@ -261,7 +232,11 @@ const Console = ({
},
{
component: ListObjects,
path: "/object-browser/:bucket?",
path: "/object-browser/:bucket",
},
{
component: ListObjects,
path: "/object-browser/:bucket/*",
},
{
component: Watch,
@@ -279,14 +254,6 @@ const Console = ({
component: Policies,
path: "/policies",
},
{
component: Trace,
path: "/trace",
},
{
component: Logs,
path: "/logs",
},
{
component: Heal,
path: "/heal",
@@ -332,7 +299,7 @@ const Console = ({
return (
<React.Fragment>
{session.status == "ok" ? (
{session.status === "ok" ? (
<div className={classes.root}>
<CssBaseline />
<Drawer
@@ -385,9 +352,7 @@ const Console = ({
/>
))}
{allowedRoutes.length > 0 ? (
<Route exact path="/">
<Redirect to={allowedRoutes[0].path} />
</Route>
<Redirect to={allowedRoutes[0].path} />
) : null}
</Switch>
</Router>

View File

@@ -20,9 +20,6 @@ import clsx from "clsx";
import Grid from "@material-ui/core/Grid";
import Paper from "@material-ui/core/Paper";
import Typography from "@material-ui/core/Typography";
import 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 api from "../../../common/api";
import { niceBytes } from "../../../common/utils";

View File

@@ -19,12 +19,15 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import {
modalBasic,
predefinedList,
} from "../Common/FormComponents/common/styleLibrary";
import api from "../../../common/api";
import UsersSelectors from "./UsersSelectors";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
interface IGroupProps {
open: boolean;
@@ -54,6 +57,7 @@ const styles = (theme: Theme) =>
textAlign: "right",
},
...modalBasic,
...predefinedList,
});
const AddGroup = ({
@@ -64,11 +68,12 @@ const AddGroup = ({
}: IGroupProps) => {
//Local States
const [groupName, setGroupName] = useState<string>("");
const [groupEnabled, setGroupEnabled] = useState<string>("");
const [groupEnabled, setGroupEnabled] = useState<boolean>(false);
const [saving, isSaving] = useState<boolean>(false);
const [addError, setError] = useState<string>("");
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
const [loadingGroup, isLoadingGroup] = useState<boolean>(false);
const [validGroup, setValidGroup] = useState<boolean>(false);
//Effects
useEffect(() => {
@@ -80,6 +85,10 @@ const AddGroup = ({
}
}, [selectedGroup]);
useEffect(() => {
setValidGroup(groupName.trim() !== "");
}, [groupName, selectedUsers]);
useEffect(() => {
if (saving) {
const saveRecord = () => {
@@ -88,7 +97,7 @@ const AddGroup = ({
.invoke("PUT", `/api/v1/groups/${groupName}`, {
group: groupName,
members: selectedUsers,
status: groupEnabled,
status: groupEnabled ? "enabled" : "disabled",
})
.then((res) => {
isSaving(false);
@@ -133,7 +142,7 @@ const AddGroup = ({
api
.invoke("GET", `/api/v1/groups/${selectedGroup}`)
.then((res: MainGroupProps) => {
setGroupEnabled(res.status);
setGroupEnabled(res.status === "enabled");
setGroupName(res.name);
setSelectedUsers(res.members);
})
@@ -153,12 +162,35 @@ const AddGroup = ({
isSaving(true);
};
const resetForm = () => {
if (selectedGroup === null) {
setGroupName("");
}
setSelectedUsers([]);
};
return (
<ModalWrapper
modalOpen={open}
onClose={closeModalAndRefresh}
title={selectedGroup !== null ? `Group Edit - ${groupName}` : "Add Group"}
title={selectedGroup !== null ? `Edit Group` : "Create Group"}
>
{selectedGroup !== null && (
<div className={classes.floatingEnabled}>
<FormSwitchWrapper
indicatorLabel={"Enabled"}
checked={groupEnabled}
value={"group_enabled"}
id="group-status"
name="group-status"
onChange={(e) => {
setGroupEnabled(e.target.checked);
}}
switchOnly
/>
</div>
)}
<form noValidate autoComplete="off" onSubmit={setSaving}>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
@@ -174,31 +206,13 @@ const AddGroup = ({
</Grid>
)}
{selectedGroup !== null ? (
<React.Fragment>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={groupEnabled}
id="group-status"
name="group-status"
label="Status"
onChange={(e) => {
setGroupEnabled(e.target.value);
}}
selectorOptions={[
{ label: "Enabled", value: "enabled" },
{ label: "Disabled", value: "disabled" },
]}
/>
</Grid>
</React.Fragment>
) : (
{selectedGroup === null ? (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="group-name"
name="group-name"
label="Name"
label="Group Name"
value={groupName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setGroupName(e.target.value);
@@ -206,23 +220,38 @@ const AddGroup = ({
/>
</Grid>
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12} className={classes.predefinedTitle}>
Group Name
</Grid>
<Grid item xs={12} className={classes.predefinedList}>
{selectedGroup}
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<UsersSelectors
selectedUsers={selectedUsers}
setSelectedUsers={setSelectedUsers}
editMode={selectedGroup !== null}
/>
</Grid>
</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={saving}
disabled={saving || !validGroup}
>
Save
</Button>

View File

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

View File

@@ -20,7 +20,6 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { LinearProgress } from "@material-ui/core";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import Title from "../../../common/Title";
import { UsersList } from "../Users/types";
import { usersSort } from "../../../utils/sortFunctions";
import api from "../../../common/api";
@@ -28,11 +27,13 @@ import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import { actionsTray } from "../Common/FormComponents/common/styleLibrary";
interface IGroupsProps {
classes: any;
selectedUsers: string[];
setSelectedUsers: any;
editMode?: boolean;
}
const styles = (theme: Theme) =>
@@ -41,10 +42,11 @@ const styles = (theme: Theme) =>
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column",
paddingTop: 15,
boxShadow: "none",
},
addSideBar: {
width: "320px",
@@ -70,36 +72,43 @@ const styles = (theme: Theme) =>
},
},
},
actionsTray: {
textAlign: "left",
"& button": {
marginLeft: 10,
},
},
filterField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: "100%",
zIndex: 500,
},
noFound: {
textAlign: "center",
padding: "10px 0",
},
tableContainer: {
maxHeight: 250,
maxHeight: 200,
},
stickyHeader: {
backgroundColor: "#fff",
},
actionsTitle: {
fontWeight: 600,
color: "#000",
fontSize: 16,
alignSelf: "center",
},
tableBlock: {
marginTop: 15,
},
filterField: {
width: 375,
fontWeight: 600,
"& .input": {
"&::placeholder": {
fontWeight: 600,
color: "#000",
},
},
},
...actionsTray,
});
const UsersSelectors = ({
classes,
selectedUsers,
setSelectedUsers,
editMode = false,
}: IGroupsProps) => {
//Local States
const [records, setRecords] = useState<any[]>([]);
@@ -166,21 +175,22 @@ const UsersSelectors = ({
return (
<React.Fragment>
<Title>Assign Users</Title>
{error !== "" ? <div>{error}</div> : <React.Fragment />}
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
{error !== "" ? <div>{error}</div> : <React.Fragment />}
{records != null && records.length > 0 ? (
<React.Fragment>
<Grid item xs={12} className={classes.actionsTray}>
<span className={classes.actionsTitle}>
{editMode ? "Edit" : "Assign"} Members
</span>
<TextField
placeholder="Filter Groups"
className={classes.filterField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
@@ -192,7 +202,7 @@ const UsersSelectors = ({
}}
/>
</Grid>
<Grid item xs={12}>
<Grid item xs={12} className={classes.tableBlock}>
<TableWrapper
columns={[{ label: "Access Key", elementKey: "accessKey" }]}
onSelect={selectionChanged}

View File

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

View File

@@ -1,276 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { AppState } from "../../../store";
import { connect } from "react-redux";
import { logMessageReceived, logResetMessages } from "./actions";
import { LogMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { timeFromDate } from "../../../common/utils";
import { isNullOrUndefined } from "util";
import { wsProtocol } from "../../../utils/wsUtils";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import { Grid } from "@material-ui/core";
import PageHeader from "../Common/PageHeader/PageHeader";
const styles = (theme: Theme) =>
createStyles({
logList: {
background: "white",
maxHeight: "400px",
overflow: "auto",
"& ul": {
margin: "4px",
padding: "0px",
},
"& ul li": {
listStyle: "none",
margin: "0px",
padding: "0px",
borderBottom: "1px solid #dedede",
},
},
tab: {
padding: "25px",
},
logerror: {
color: "#A52A2A",
},
logerror_tab: {
color: "#A52A2A",
padding: "25px",
},
ansidefault: {
color: "black",
},
...containerForHeader(theme.spacing(4)),
});
interface ILogs {
classes: any;
logMessageReceived: typeof logMessageReceived;
logResetMessages: typeof logResetMessages;
messages: LogMessage[];
}
const Logs = ({
classes,
logMessageReceived,
logResetMessages,
messages,
}: ILogs) => {
useEffect(() => {
logResetMessages();
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket(
`${wsProt}://${url.hostname}:${port}/ws/console`
);
let interval: any | null = null;
if (c !== null) {
c.onopen = () => {
console.log("WebSocket Client Connected");
c.send("ok");
interval = setInterval(() => {
c.send("ok");
}, 10 * 1000);
};
c.onmessage = (message: IMessageEvent) => {
// console.log(message.data.toString())
let m: LogMessage = JSON.parse(message.data.toString());
m.time = new Date(m.time.toString());
m.key = Math.random();
logMessageReceived(m);
};
c.onclose = () => {
clearInterval(interval);
console.log("connection closed by server");
};
return () => {
c.close(1000);
clearInterval(interval);
console.log("closing websockets");
};
}
}, [logMessageReceived]);
// replaces a character of a string with other at a given index
const replaceWeirdChar = (
origString: string,
replaceChar: string,
index: number
) => {
let firstPart = origString.substr(0, index);
let lastPart = origString.substr(index + 1);
let newString = firstPart + replaceChar + lastPart;
return newString;
};
const renderError = (logElement: LogMessage) => {
let errorElems = [];
if (!isNullOrUndefined(logElement.error)) {
if (logElement.api && logElement.api.name) {
errorElems.push(
<li key={`api-${logElement.key}`}>
<span className={classes.logerror}>API: {logElement.api.name}</span>
</li>
);
}
if (logElement.time) {
errorElems.push(
<li key={`time-${logElement.key}`}>
<span className={classes.logerror}>
Time: {timeFromDate(logElement.time)}
</span>
</li>
);
}
if (logElement.deploymentid) {
errorElems.push(
<li key={`deploytmentid-${logElement.key}`}>
<span className={classes.logerror}>
DeploymentID: {logElement.deploymentid}
</span>
</li>
);
}
if (logElement.requestID) {
errorElems.push(
<li key={`requestid-${logElement.key}`}>
<span className={classes.logerror}>
RequestID: {logElement.requestID}
</span>
</li>
);
}
if (logElement.remotehost) {
errorElems.push(
<li key={`remotehost-${logElement.key}`}>
<span className={classes.logerror}>
RemoteHost: {logElement.remotehost}
</span>
</li>
);
}
if (logElement.host) {
errorElems.push(
<li key={`host-${logElement.key}`}>
<span className={classes.logerror}>Host: {logElement.host}</span>
</li>
);
}
if (logElement.userAgent) {
errorElems.push(
<li key={`useragent-${logElement.key}`}>
<span className={classes.logerror}>
UserAgent: {logElement.userAgent}
</span>
</li>
);
}
if (logElement.error.message) {
errorElems.push(
<li key={`message-${logElement.key}`}>
<span className={classes.logerror}>
Error: {logElement.error.message}
</span>
</li>
);
}
if (logElement.error.source) {
// for all sources add padding
for (let s in logElement.error.source) {
errorElems.push(
<li key={`source-${logElement.key}-${s}`}>
<span className={classes.logerror_tab}>
{logElement.error.source[s]}
</span>
</li>
);
}
}
}
return errorElems;
};
const renderLog = (logElement: LogMessage) => {
let logMessage = logElement.ConsoleMsg;
// remove any non ascii characters, exclude any control codes
logMessage = logMessage.replace(/([^\x20-\x7F])/g, "");
// regex for terminal colors like e.g. `[31;4m `
const tColorRegex = /((\[[0-9;]+m))/g;
// get substring if there was a match for to split what
// is going to be colored and what not, here we add color
// only to the first match.
let substr = logMessage.replace(tColorRegex, "");
// if starts with multiple spaces add padding
if (substr.startsWith(" ")) {
return (
<li key={logElement.key}>
<span className={classes.tab}>{substr}</span>
</li>
);
} else if (!isNullOrUndefined(logElement.error)) {
// list error message and all sources and error elems
return renderError(logElement);
} else {
// for all remaining set default class
return (
<li key={logElement.key}>
<span className={classes.ansidefault}>{substr}</span>
</li>
);
}
};
return (
<React.Fragment>
<PageHeader label="Logs" />
<Grid container>
<Grid className={classes.container} item xs={12}>
<div className={classes.logList}>
<ul>
{messages.map((m) => {
return renderLog(m);
})}
</ul>
</div>
</Grid>
</Grid>
</React.Fragment>
);
};
const mapState = (state: AppState) => ({
messages: state.logs.messages,
});
const connector = connect(mapState, {
logMessageReceived: logMessageReceived,
logResetMessages: logResetMessages,
});
export default connector(withStyles(styles)(Logs));

View File

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

View File

@@ -42,6 +42,7 @@ import {
removeEmptyFields,
} from "../Configurations/utils";
import { IElementValue } from "../Configurations/types";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
@@ -60,6 +61,70 @@ const styles = (theme: Theme) =>
logoButton: {
height: "80px",
},
lambdaNotif: {
backgroundColor: "#fff",
border: "#393939 1px solid",
borderRadius: 5,
width: 101,
height: 91,
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: 16,
cursor: "pointer",
"& img": {
maxWidth: 71,
maxHeight: 71,
},
},
iconContainer: {
display: "flex",
flexDirection: "row",
width: 455,
justifyContent: "space-between",
flexWrap: "wrap",
},
nonIconContainer: {
marginBottom: 16,
"& button": {
marginRight: 16,
},
},
pickTitle: {
fontWeight: 600,
color: "#393939",
fontSize: 14,
marginBottom: 16,
},
lambdaFormIndicator: {
display: "flex",
marginBottom: 40,
},
lambdaName: {
fontSize: 18,
fontWeight: 700,
color: "#000",
marginBottom: 6,
},
lambdaSubname: {
fontSize: 12,
color: "#000",
fontWeight: 600,
},
lambdaIcon: {
borderRadius: 5,
border: "#393939 1px solid",
width: 53,
height: 48,
display: "flex",
justifyContent: "center",
alignItems: "center",
marginRight: 16,
"& img": {
width: 38,
},
},
...modalBasic,
});
interface IAddNotificationEndpointProps {
@@ -136,186 +201,111 @@ const AddNotificationEndpoint = ({
}
}
let targetTitle = "";
switch (service) {
case notifyNsq:
targetTitle = "NSQ";
break;
case notifyWebhooks:
targetTitle = "Webhooks";
break;
case notifyElasticsearch:
targetTitle = "Elastic Search";
break;
case notifyNats:
targetTitle = "NATS";
break;
case notifyMqtt:
targetTitle = "MQTT";
break;
case notifyRedis:
targetTitle = "Redis";
break;
case notifyKafka:
targetTitle = "Kafka";
break;
case notifyPostgres:
targetTitle = "Postgres";
break;
case notifyMysql:
targetTitle = "Mysql";
break;
case notifyAmqp:
targetTitle = "AMQP";
break;
}
const servicesList = [
{
actionTrigger: notifyPostgres,
targetTitle: "Postgres SQL",
logo: "/postgres.png",
},
{
actionTrigger: notifyKafka,
targetTitle: "Kafka",
logo: "/kafka.png",
},
{
actionTrigger: notifyAmqp,
targetTitle: "AMQP",
logo: "/amqp.png",
},
{
actionTrigger: notifyMqtt,
targetTitle: "MQTT",
logo: "/mqtt.png",
},
{
actionTrigger: notifyRedis,
targetTitle: "Redis",
logo: "/redis.png",
},
{
actionTrigger: notifyNats,
targetTitle: "NATS",
logo: "/nats.png",
},
{
actionTrigger: notifyMysql,
targetTitle: "Mysql",
logo: "/mysql.png",
},
{
actionTrigger: notifyElasticsearch,
targetTitle: "Elastic Search",
logo: "/elasticsearch.png",
},
{
actionTrigger: notifyWebhooks,
targetTitle: "Webhook",
logo: "",
},
{
actionTrigger: notifyNsq,
targetTitle: "NSQ",
logo: "",
},
];
const nonLogos = servicesList.filter((elService) => elService.logo === "");
const withLogos = servicesList.filter((elService) => elService.logo !== "");
const targetElement = servicesList.find(
(element) => element.actionTrigger === service
);
const goBack = () => {
setService("");
};
return (
<ModalWrapper
modalOpen={open}
onClose={closeModalAndRefresh}
title={`Add Lambda Notification Target ${targetTitle}`}
>
<ModalWrapper modalOpen={open} onClose={closeModalAndRefresh} title={""}>
{service === "" && (
<Grid container>
<Grid item xs={12}>
<p>Pick a supported service:</p>
<table className={classes.chooseTable} style={{ width: "100%" }}>
<tbody>
<tr>
<td>
<Button
onClick={() => {
setService(notifyPostgres);
}}
>
<img
src="/postgres.png"
className={classes.logoButton}
alt="postgres"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyKafka);
}}
>
<img
src="/kafka.png"
className={classes.logoButton}
alt="kafka"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyAmqp);
}}
>
<img
src="/amqp.png"
className={classes.logoButton}
alt="amqp"
/>
</Button>
</td>
</tr>
<tr>
<td>
<Button
onClick={() => {
setService(notifyMqtt);
}}
>
<img
src="/mqtt.png"
className={classes.logoButton}
alt="mqtt"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyRedis);
}}
>
<img
src="/redis.png"
className={classes.logoButton}
alt="redis"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyNats);
}}
>
<img
src="/nats.png"
className={classes.logoButton}
alt="nats"
/>
</Button>
</td>
</tr>
<tr>
<td>
<Button
onClick={() => {
setService(notifyMysql);
}}
>
<img
src="/mysql.png"
className={classes.logoButton}
alt="mysql"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyElasticsearch);
}}
>
<img
src="/elasticsearch.png"
className={classes.logoButton}
alt="elasticsearch"
/>
</Button>
</td>
<td></td>
</tr>
<tr>
<td>
<Button
onClick={() => {
setService(notifyWebhooks);
}}
>
Webhook
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyNsq);
}}
>
NSQ
</Button>
</td>
<td />
</tr>
</tbody>
</table>
<div className={classes.pickTitle}>Pick a supported service:</div>
<div className={classes.nonIconContainer}>
{nonLogos.map((item) => {
return (
<Button
variant="contained"
color="primary"
key={`non-icon-${item.targetTitle}`}
onClick={() => {
setService(item.actionTrigger);
}}
>
{item.targetTitle.toUpperCase()}
</Button>
);
})}
</div>
<div className={classes.iconContainer}>
{withLogos.map((item) => {
return (
<button
key={`icon-${item.targetTitle}`}
className={classes.lambdaNotif}
onClick={() => {
setService(item.actionTrigger);
}}
>
<img
src={item.logo}
className={classes.logoButton}
alt={item.targetTitle}
/>
</button>
);
})}
</div>
</Grid>
<Grid item xs={12}>
<br />
@@ -342,10 +332,37 @@ const AddNotificationEndpoint = ({
</Grid>
)}
<form noValidate onSubmit={submitForm}>
<Grid item xs={12} className={classes.lambdaFormIndicator}>
{targetElement && targetElement.logo !== "" && (
<div className={classes.lambdaIcon}>
<img
src={targetElement.logo}
alt={targetElement.targetTitle}
/>
</div>
)}
<div className={classes.lambdaTitle}>
<div className={classes.lambdaName}>
{targetElement ? targetElement.targetTitle : ""}
</div>
<div className={classes.lambdaSubname}>
Add Lambda Notification Target
</div>
</div>
</Grid>
<Grid item xs={12}>
{srvComponent}
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<button
type="button"
color="primary"
className={classes.clearButton}
onClick={goBack}
>
Back
</button>
<Button
type="submit"
variant="contained"

View File

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

View File

@@ -15,11 +15,11 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import AddBucket from "../Buckets/ListBuckets/AddBucket";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
@@ -28,13 +28,16 @@ import { CreateIcon } from "../../../icons";
import { niceBytes } from "../../../common/utils";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import { Bucket, BucketList } from "../Buckets/types";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import api from "../../../common/api";
import history from "../../../history";
import {
actionsTray,
objectBrowserCommon,
searchField,
} 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) =>
createStyles({
@@ -68,20 +71,47 @@ const styles = (theme: Theme) =>
},
usedSpaceCol: {
width: 150,
textAlign: "right",
},
subTitleLabel: {
alignItems: "center",
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,
...searchField,
...objectBrowserCommon,
});
interface IBrowseBucketsProps {
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 [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
@@ -92,6 +122,10 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
const offset = page * rowsPerPage;
useEffect(() => {
resetRoutesList(true);
}, [match]);
useEffect(() => {
if (loading) {
api
@@ -119,21 +153,24 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
setError(err);
});
}
}, [loading]);
}, [loading, offset, rowsPerPage, page]);
const closeAddModalAndRefresh = () => {
const closeAddModalAndRefresh = (refresh: boolean) => {
setAddScreenOpen(false);
setLoading(false);
if (refresh) {
setLoading(true);
}
};
const filteredRecords = records
.filter((b: Bucket) => {
if (filterBuckets === "") {
return true;
}
return b.name.indexOf(filterBuckets) >= 0;
})
.slice(offset, offset + rowsPerPage);
const filteredRecords = records.filter((b: Bucket) => {
if (filterBuckets === "") {
return true;
}
return b.name.indexOf(filterBuckets) >= 0;
});
const showInPage = filteredRecords.slice(offset, offset + rowsPerPage);
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
@@ -147,6 +184,22 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
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 (
<React.Fragment>
{addScreenOpen && (
@@ -156,10 +209,24 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
/>
)}
<Grid container>
<Grid item xs={2} className={classes.subTitleLabel}>
<Typography variant="h6">Buckets</Typography>
<Grid item xs={12} className={classes.obTitleSection}>
<div>
<BrowserBreadcrumbs />
</div>
<div>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Create Bucket
</Button>
</div>
</Grid>
<Grid item xs={10} className={classes.actionsTray}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Buckets"
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 item xs={12}>
<br />
@@ -195,19 +252,28 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
{error !== "" && <span className={classes.errorBlock}>{error}</span>}
<TableWrapper
itemActions={[
{ type: "view", to: `/object-browser`, sendOnlyId: true },
{
type: "view",
sendOnlyId: true,
onClick: handleViewChange,
},
]}
columns={[
{ label: "Name", elementKey: "name" },
{
label: "Name",
elementKey: "name",
renderFunction: renderBucket,
},
{
label: "Used Space",
elementKey: "size",
renderFunction: niceBytes,
globalClass: classes.usedSpaceCol,
rowClass: classes.usedSpaceCol,
},
]}
isLoading={loading}
records={filteredRecords}
records={showInPage}
entityName="Buckets"
idField="name"
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 get from "lodash/get";
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 { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader";
@@ -73,7 +73,7 @@ const styles = (theme: Theme) =>
});
const ObjectBrowser = ({ match, classes }: IObjectBrowserProps) => {
const pathIn = get(match, "path", "");
const pathIn = get(match, "url", "");
return (
<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

@@ -16,19 +16,18 @@
import React from "react";
import Grid from "@material-ui/core/Grid";
import { UnControlled as CodeMirror } from "react-codemirror2";
import Typography from "@material-ui/core/Typography";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../common/api";
import "codemirror/lib/codemirror.css";
import "codemirror/theme/material.css";
import { Policy } from "./types";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import {
fieldBasic,
modalBasic,
} from "../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
require("codemirror/mode/javascript/javascript");
import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper";
const styles = (theme: Theme) =>
createStyles({
@@ -39,26 +38,11 @@ const styles = (theme: Theme) =>
minHeight: 400,
width: "100%",
},
codeMirror: {
fontSize: 14,
"& .CodeMirror": {
color: "#fff",
backgroundColor: "#081C42",
},
"& .CodeMirror-gutter": {
backgroundColor: "#081C4280",
},
"& .CodeMirror-linenumber": {
color: "#000",
fontSize: 10,
height: 20,
lineHeight: "20px",
},
},
buttonContainer: {
textAlign: "right",
},
...modalBasic,
...fieldBasic,
});
interface IAddPolicyProps {
@@ -121,13 +105,26 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
if (policyEdit) {
this.setState({
policyName: policyEdit.name,
policyDefinition: policyEdit
? JSON.stringify(JSON.parse(policyEdit.policy), null, 4)
: "",
});
}
}
resetForm() {
this.setState({
policyName: "",
policyDefinition: "",
});
}
render() {
const { classes, open, policyEdit } = this.props;
const { addLoading, addError, policyName } = this.state;
const { addLoading, addError, policyName, policyDefinition } = this.state;
const validSave = policyName.trim() !== "";
return (
<ModalWrapper
modalOpen={open}
@@ -163,6 +160,7 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
id="policy-name"
name="policy-name"
label="Policy Name"
placeholder="Enter Policy Name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ policyName: e.target.value });
}}
@@ -173,31 +171,32 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<CodeMirror
className={classes.codeMirror}
value={
policyEdit
? JSON.stringify(JSON.parse(policyEdit.policy), null, 4)
: ""
}
options={{
mode: "javascript",
lineNumbers: true,
}}
onChange={(editor, data, value) => {
this.setState({ policyDefinition: value });
}}
/>
</Grid>
<CodeMirrorWrapper
label="Write Policy"
value={policyDefinition}
onBeforeChange={(editor, data, value) => {
this.setState({ policyDefinition: value });
}}
readOnly={!!policyEdit}
/>
</Grid>
{!policyEdit && (
<Grid item xs={12} className={classes.buttonContainer}>
<button
type="button"
color="primary"
className={classes.clearButton}
onClick={() => {
this.resetForm();
}}
>
Clear
</button>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
disabled={addLoading || !validSave}
>
Save
</Button>

View File

@@ -23,7 +23,7 @@ import {
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress
LinearProgress,
} from "@material-ui/core";
import api from "../../../common/api";
import { PolicyList } from "./types";
@@ -32,8 +32,8 @@ import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
});
interface IDeletePolicyProps {
@@ -54,7 +54,7 @@ class DeletePolicy extends React.Component<
> {
state: IDeletePolicyState = {
deleteLoading: false,
deleteError: ""
deleteError: "",
};
removeRecord() {
const { deleteLoading } = this.state;
@@ -69,17 +69,17 @@ class DeletePolicy extends React.Component<
this.setState(
{
deleteLoading: false,
deleteError: ""
deleteError: "",
},
() => {
this.props.closeDeleteModalAndRefresh(true);
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
deleteLoading: false,
deleteError: err
deleteError: err,
});
});
});
@@ -98,7 +98,7 @@ class DeletePolicy extends React.Component<
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Bucket</DialogTitle>
<DialogTitle id="alert-dialog-title">Delete Policy</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">

View File

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

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