Compare commits

...

12 Commits

Author SHA1 Message Date
Daniel Valdivia
27fd91ed37 Release v0.22.2 (#2495)
Signed-off-by: Daniel Valdivia
<18384552+dvaldivia@users.noreply.github.com>
2022-12-09 16:42:57 -08:00
Alex
7df0560104 Updated mds & fixed blank pages on login (#2494)
- Updates mds to v0.0.7
- Fixed an issue with blank pages during login
- Fixes an issue during logout where path was redirected to
'logout/login'

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
2022-12-09 14:24:58 -08:00
Javier Adriel
2368199e03 IDP management UI (#2487)
Adds UI to interact with IDP Configurations (CRUD)
2022-12-09 14:13:10 -08:00
Daniel Valdivia
bee98e1ba0 fix: race Condition on Object Browser via Websocket (#2492)
Signed-off-by: Daniel Valdivia
<18384552+dvaldivia@users.noreply.github.com>
2022-12-09 11:18:19 -08:00
Harshavardhana
8abbbb4625 avoid printing random logs (#2489) 2022-12-08 07:54:21 -08:00
Javier Adriel
ef182fe75e Increase threshold to 54.4 (Current coverage 54.5) (#2488) 2022-12-07 20:01:09 -06:00
Daniel Valdivia
613e93fdc6 Release v0.22.1 (#2486)
Signed-off-by: Daniel Valdivia <hola@danielvaldivia.com>
2022-12-06 14:41:25 -08:00
Javier Adriel
a5e3c89a54 Implement handlers for managing IDP (OpenID and LDAP) Configurations (#2485) 2022-12-06 14:33:17 -06:00
jinapurapu
7f55b71495 Add backend tier status checking to all tier types (#2420)
Moves Tier status check to backend for all Tier types.

![Screen Shot 2022-12-02 at 10 37 36
AM](https://user-images.githubusercontent.com/65002498/205370942-e20c0229-77b1-4a7c-9579-eebbd101c4ae.png)
![Screen Shot 2022-12-02 at 10 37 18
AM](https://user-images.githubusercontent.com/65002498/205370943-0d038206-b0e3-4597-9522-b21f94c36c0b.png)

Co-authored-by: Jillian Inapurapu <jillii@Jillians-MBP.attlocal.net>
Co-authored-by: Alex <33497058+bexsoft@users.noreply.github.com>
2022-12-06 12:13:14 -08:00
Alex
d95d59e454 Changed Object browser logic to work with websockets (#2419)
fixes https://github.com/minio/console/issues/943

## What does this do?

This allows us to start streaming results to the list instead of waiting
to retrieve all the objects list before sending it to the client

Included a couple of fixes:

- Removed metadata for deleted items
- Fixed multiple metadata requests
- Fixed height grow for parent wrapper
- Fixed object reload after restore version


Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
2022-12-06 11:23:07 -08:00
Harshavardhana
08ea069ed4 change doc URL if UI is running in k8s (#2484)
Co-authored-by: Prakash Senthil Vel <23444145+prakashsvmx@users.noreply.github.com>
2022-12-06 09:53:01 -08:00
Javier Adriel
e7a41b4cd9 Call end_session_endpoint in IDP provider when login out from Console (#2476) 2022-12-05 18:14:41 -06:00
142 changed files with 7701 additions and 866 deletions

View File

@@ -2517,7 +2517,7 @@ jobs:
go tool cover -func=all.out | grep total > tmp2
result=`cat tmp2 | awk 'END {print $3}'`
result=${result%\%}
threshold=53.40
threshold=54.40
echo "Result:"
echo "$result%"
if (( $(echo "$result >= $threshold" |bc -l) )); then

View File

@@ -116,8 +116,8 @@ func TestLogout(t *testing.T) {
log.Println("authentication token not found in cookies response")
return
}
request, err = http.NewRequest("POST", "http://localhost:9090/api/v1/logout", requestDataBody)
logoutRequest := bytes.NewReader([]byte("{}"))
request, err = http.NewRequest("POST", "http://localhost:9090/api/v1/logout", logoutRequest)
if err != nil {
log.Println(err)
return
@@ -126,7 +126,6 @@ func TestLogout(t *testing.T) {
request.Header.Add("Content-Type", "application/json")
response, err = client.Do(request)
assert.NotNil(response, "Logout response is nil")
assert.Nil(err, "Logout errored out")
assert.Equal(response.StatusCode, 200)

View File

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

View File

@@ -32,7 +32,7 @@ spec:
spec:
containers:
- name: console
image: 'minio/console:v0.22.0'
image: 'minio/console:v0.22.2'
imagePullPolicy: "IfNotPresent"
env:
- name: CONSOLE_MINIO_SERVER

View File

@@ -0,0 +1,133 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 (
"context"
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// IdpListConfigurationsResponse idp list configurations response
//
// swagger:model idpListConfigurationsResponse
type IdpListConfigurationsResponse struct {
// results
Results []*IdpServerConfiguration `json:"results"`
}
// Validate validates this idp list configurations response
func (m *IdpListConfigurationsResponse) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateResults(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *IdpListConfigurationsResponse) validateResults(formats strfmt.Registry) error {
if swag.IsZero(m.Results) { // not required
return nil
}
for i := 0; i < len(m.Results); i++ {
if swag.IsZero(m.Results[i]) { // not required
continue
}
if m.Results[i] != nil {
if err := m.Results[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("results" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("results" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// ContextValidate validate this idp list configurations response based on the context it is used
func (m *IdpListConfigurationsResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateResults(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *IdpListConfigurationsResponse) contextValidateResults(ctx context.Context, formats strfmt.Registry) error {
for i := 0; i < len(m.Results); i++ {
if m.Results[i] != nil {
if err := m.Results[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("results" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("results" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *IdpListConfigurationsResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *IdpListConfigurationsResponse) UnmarshalBinary(b []byte) error {
var res IdpListConfigurationsResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,145 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 (
"context"
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// IdpServerConfiguration idp server configuration
//
// swagger:model idpServerConfiguration
type IdpServerConfiguration struct {
// enabled
Enabled bool `json:"enabled,omitempty"`
// info
Info []*IdpServerConfigurationInfo `json:"info"`
// input
Input string `json:"input,omitempty"`
// name
Name string `json:"name,omitempty"`
// type
Type string `json:"type,omitempty"`
}
// Validate validates this idp server configuration
func (m *IdpServerConfiguration) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateInfo(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *IdpServerConfiguration) validateInfo(formats strfmt.Registry) error {
if swag.IsZero(m.Info) { // not required
return nil
}
for i := 0; i < len(m.Info); i++ {
if swag.IsZero(m.Info[i]) { // not required
continue
}
if m.Info[i] != nil {
if err := m.Info[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("info" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("info" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// ContextValidate validate this idp server configuration based on the context it is used
func (m *IdpServerConfiguration) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateInfo(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *IdpServerConfiguration) contextValidateInfo(ctx context.Context, formats strfmt.Registry) error {
for i := 0; i < len(m.Info); i++ {
if m.Info[i] != nil {
if err := m.Info[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("info" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("info" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *IdpServerConfiguration) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *IdpServerConfiguration) UnmarshalBinary(b []byte) error {
var res IdpServerConfiguration
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,76 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// IdpServerConfigurationInfo idp server configuration info
//
// swagger:model idpServerConfigurationInfo
type IdpServerConfigurationInfo struct {
// is cfg
IsCfg bool `json:"isCfg,omitempty"`
// is env
IsEnv bool `json:"isEnv,omitempty"`
// key
Key string `json:"key,omitempty"`
// value
Value string `json:"value,omitempty"`
}
// Validate validates this idp server configuration info
func (m *IdpServerConfigurationInfo) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this idp server configuration info based on context it is used
func (m *IdpServerConfigurationInfo) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *IdpServerConfigurationInfo) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *IdpServerConfigurationInfo) UnmarshalBinary(b []byte) error {
var res IdpServerConfigurationInfo
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -41,6 +41,9 @@ type LoginDetails struct {
// is direct p v
IsDirectPV bool `json:"isDirectPV,omitempty"`
// is k8 s
IsK8S bool `json:"isK8S,omitempty"`
// login strategy
// Enum: [form redirect service-account redirect-service-account]
LoginStrategy string `json:"loginStrategy,omitempty"`

View File

@@ -34,6 +34,9 @@ import (
// swagger:model loginResponse
type LoginResponse struct {
// ID p refresh token
IDPRefreshToken string `json:"IDPRefreshToken,omitempty"`
// session Id
SessionID string `json:"sessionId,omitempty"`
}

67
models/logout_request.go Normal file
View File

@@ -0,0 +1,67 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// LogoutRequest logout request
//
// swagger:model logoutRequest
type LogoutRequest struct {
// state
State string `json:"state,omitempty"`
}
// Validate validates this logout request
func (m *LogoutRequest) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this logout request based on context it is used
func (m *LogoutRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *LogoutRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *LogoutRequest) UnmarshalBinary(b []byte) error {
var res LogoutRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -40,6 +40,9 @@ type RewindItem struct {
// delete flag
DeleteFlag bool `json:"delete_flag,omitempty"`
// is latest
IsLatest bool `json:"is_latest,omitempty"`
// last modified
LastModified string `json:"last_modified,omitempty"`

View File

@@ -0,0 +1,67 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// SetIDPResponse set ID p response
//
// swagger:model setIDPResponse
type SetIDPResponse struct {
// restart
Restart bool `json:"restart,omitempty"`
}
// Validate validates this set ID p response
func (m *SetIDPResponse) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this set ID p response based on context it is used
func (m *SetIDPResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *SetIDPResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *SetIDPResponse) UnmarshalBinary(b []byte) error {
var res SetIDPResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -3626,6 +3626,9 @@ func init() {
"isDirectPV": {
"type": "boolean"
},
"isK8S": {
"type": "boolean"
},
"loginStrategy": {
"type": "string",
"enum": [
@@ -9622,6 +9625,9 @@ func init() {
"isDirectPV": {
"type": "boolean"
},
"isK8S": {
"type": "boolean"
},
"loginStrategy": {
"type": "string",
"enum": [

View File

@@ -25,6 +25,7 @@ import (
xoauth2 "golang.org/x/oauth2"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/pkg/env"
"github.com/minio/console/restapi"
@@ -93,6 +94,15 @@ func login(credentials restapi.ConsoleCredentialsI) (*string, error) {
return &token, nil
}
// isKubernetes returns true if minio is running in kubernetes.
func isKubernetes() bool {
// Kubernetes env used to validate if we are
// indeed running inside a kubernetes pod
// is KUBERNETES_SERVICE_HOST
// https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet_pods.go#L541
return env.Get("KUBERNETES_SERVICE_HOST", "") != ""
}
// getLoginDetailsResponse returns information regarding the Console authentication mechanism.
func getLoginDetailsResponse(params authApi.LoginDetailParams) (*models.LoginDetails, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
@@ -129,6 +139,7 @@ func getLoginDetailsResponse(params authApi.LoginDetailParams) (*models.LoginDet
LoginStrategy: loginStrategy,
RedirectRules: redirectRules,
IsDirectPV: getDirectPVEnabled(),
IsK8S: isKubernetes(),
}
return loginDetails, nil
}

View File

@@ -37,6 +37,7 @@ type ProviderConfig struct {
Userinfo bool
RedirectCallbackDynamic bool
RedirectCallback string
EndSessionEndpoint string
RoleArn string // can be empty
}

View File

@@ -110,6 +110,7 @@ type Provider struct {
IDPName string
// if enabled means that we need extrace access_token as well
UserInfo bool
RefreshToken string
oauth2Config Configuration
provHTTPClient *http.Client
}
@@ -319,6 +320,7 @@ func (client *Provider) VerifyIdentity(ctx context.Context, code, state, roleARN
getWebTokenExpiry := func() (*credentials.WebIdentityToken, error) {
customCtx := context.WithValue(ctx, oauth2.HTTPClient, client.provHTTPClient)
oauth2Token, err := client.oauth2Config.Exchange(customCtx, code)
client.RefreshToken = oauth2Token.RefreshToken
if err != nil {
return nil, err
}

View File

@@ -1,9 +1,9 @@
{
"files": {
"main.css": "./static/css/main.b20a708b.css",
"main.js": "./static/js/main.ef5eea8b.js",
"static/js/1260.5248e62a.chunk.js": "./static/js/1260.5248e62a.chunk.js",
"static/js/6914.8c73a010.chunk.js": "./static/js/6914.8c73a010.chunk.js",
"main.js": "./static/js/main.2b781eaf.js",
"static/js/1260.ded6e17b.chunk.js": "./static/js/1260.ded6e17b.chunk.js",
"static/js/6914.1699f207.chunk.js": "./static/js/6914.1699f207.chunk.js",
"static/js/4209.227aa3b5.chunk.js": "./static/js/4209.227aa3b5.chunk.js",
"static/js/1829.be0f91b9.chunk.js": "./static/js/1829.be0f91b9.chunk.js",
"static/js/4455.237b7d46.chunk.js": "./static/js/4455.237b7d46.chunk.js",
@@ -28,7 +28,7 @@
"static/css/5503.9cb5f34b.chunk.css": "./static/css/5503.9cb5f34b.chunk.css",
"static/js/5503.8102c269.chunk.js": "./static/js/5503.8102c269.chunk.js",
"static/js/5926.a85e7cf5.chunk.js": "./static/js/5926.a85e7cf5.chunk.js",
"static/js/701.6d0fe16f.chunk.js": "./static/js/701.6d0fe16f.chunk.js",
"static/js/701.34df1a0d.chunk.js": "./static/js/701.34df1a0d.chunk.js",
"static/js/7821.461c5aa3.chunk.js": "./static/js/7821.461c5aa3.chunk.js",
"static/css/2850.9cb5f34b.chunk.css": "./static/css/2850.9cb5f34b.chunk.css",
"static/js/2850.c2c3a292.chunk.js": "./static/js/2850.c2c3a292.chunk.js",
@@ -40,6 +40,12 @@
"static/css/5517.9cb5f34b.chunk.css": "./static/css/5517.9cb5f34b.chunk.css",
"static/js/5517.730e0aeb.chunk.js": "./static/js/5517.730e0aeb.chunk.js",
"static/js/2555.47860755.chunk.js": "./static/js/2555.47860755.chunk.js",
"static/js/7486.83e0d248.chunk.js": "./static/js/7486.83e0d248.chunk.js",
"static/js/1377.6fbc40f3.chunk.js": "./static/js/1377.6fbc40f3.chunk.js",
"static/js/4672.7e50d90b.chunk.js": "./static/js/4672.7e50d90b.chunk.js",
"static/js/2516.5d118ef6.chunk.js": "./static/js/2516.5d118ef6.chunk.js",
"static/js/2759.3ab5d8ec.chunk.js": "./static/js/2759.3ab5d8ec.chunk.js",
"static/js/7295.c85034bf.chunk.js": "./static/js/7295.c85034bf.chunk.js",
"static/js/7585.cd3ad44f.chunk.js": "./static/js/7585.cd3ad44f.chunk.js",
"static/js/4902.b5c5ff07.chunk.js": "./static/js/4902.b5c5ff07.chunk.js",
"static/js/7847.659fc617.chunk.js": "./static/js/7847.659fc617.chunk.js",
@@ -61,9 +67,9 @@
"static/js/8833.9dccd2d5.chunk.js": "./static/js/8833.9dccd2d5.chunk.js",
"static/js/6526.e3612299.chunk.js": "./static/js/6526.e3612299.chunk.js",
"static/js/483.5b997456.chunk.js": "./static/js/483.5b997456.chunk.js",
"static/js/9467.42dc3ff9.chunk.js": "./static/js/9467.42dc3ff9.chunk.js",
"static/js/9467.8e1b707f.chunk.js": "./static/js/9467.8e1b707f.chunk.js",
"static/js/6895.488a4025.chunk.js": "./static/js/6895.488a4025.chunk.js",
"static/js/1379.3fee0dea.chunk.js": "./static/js/1379.3fee0dea.chunk.js",
"static/js/5882.41668ace.chunk.js": "./static/js/5882.41668ace.chunk.js",
"static/js/8277.e772e8bd.chunk.js": "./static/js/8277.e772e8bd.chunk.js",
"static/js/4133.2386eedc.chunk.js": "./static/js/4133.2386eedc.chunk.js",
"static/css/1367.9cb5f34b.chunk.css": "./static/css/1367.9cb5f34b.chunk.css",
@@ -104,8 +110,8 @@
"static/js/9179.73844de6.chunk.js": "./static/js/9179.73844de6.chunk.js",
"static/js/51.f63429fd.chunk.js": "./static/js/51.f63429fd.chunk.js",
"static/js/711.7013b9d7.chunk.js": "./static/js/711.7013b9d7.chunk.js",
"static/js/6901.e2472ebd.chunk.js": "./static/js/6901.e2472ebd.chunk.js",
"static/js/2185.164ae281.chunk.js": "./static/js/2185.164ae281.chunk.js",
"static/js/6901.cda3f1f9.chunk.js": "./static/js/6901.cda3f1f9.chunk.js",
"static/js/2185.4baca582.chunk.js": "./static/js/2185.4baca582.chunk.js",
"static/js/312.770148c8.chunk.js": "./static/js/312.770148c8.chunk.js",
"static/js/2112.c85537ec.chunk.js": "./static/js/2112.c85537ec.chunk.js",
"static/js/4619.bbf03503.chunk.js": "./static/js/4619.bbf03503.chunk.js",
@@ -151,8 +157,10 @@
"static/js/9515.a4e964be.chunk.js": "./static/js/9515.a4e964be.chunk.js",
"static/js/2983.e248775f.chunk.js": "./static/js/2983.e248775f.chunk.js",
"static/js/5861.65847210.chunk.js": "./static/js/5861.65847210.chunk.js",
"static/js/537.85347210.chunk.js": "./static/js/537.85347210.chunk.js",
"static/js/2763.08c6e1fd.chunk.js": "./static/js/2763.08c6e1fd.chunk.js",
"static/media/LoginBG.png": "./static/media/LoginBG.143407455857f28ba809.png",
"static/media/videoBG.mp4": "./static/media/videoBG.17363418b3c2246a0e27.mp4",
"static/media/loginAnimationPoster.png": "./static/media/loginAnimationPoster.9aa924bfe619e71d5d29.png",
"static/media/Inter-BoldItalic.woff": "./static/media/Inter-BoldItalic.b376885042f6c961a541.woff",
"static/media/Inter-LightItalic.woff": "./static/media/Inter-LightItalic.ef9f65d91d2b0ba9b2e4.woff",
"static/media/Inter-BlackItalic.woff": "./static/media/Inter-BlackItalic.ca1e738e4f349f27514d.woff",
@@ -173,9 +181,9 @@
"static/media/Inter-Regular.woff2": "./static/media/Inter-Regular.c8ba52b05a9ef10f4758.woff2",
"index.html": "./index.html",
"main.b20a708b.css.map": "./static/css/main.b20a708b.css.map",
"main.ef5eea8b.js.map": "./static/js/main.ef5eea8b.js.map",
"1260.5248e62a.chunk.js.map": "./static/js/1260.5248e62a.chunk.js.map",
"6914.8c73a010.chunk.js.map": "./static/js/6914.8c73a010.chunk.js.map",
"main.2b781eaf.js.map": "./static/js/main.2b781eaf.js.map",
"1260.ded6e17b.chunk.js.map": "./static/js/1260.ded6e17b.chunk.js.map",
"6914.1699f207.chunk.js.map": "./static/js/6914.1699f207.chunk.js.map",
"4209.227aa3b5.chunk.js.map": "./static/js/4209.227aa3b5.chunk.js.map",
"1829.be0f91b9.chunk.js.map": "./static/js/1829.be0f91b9.chunk.js.map",
"4455.237b7d46.chunk.js.map": "./static/js/4455.237b7d46.chunk.js.map",
@@ -200,7 +208,7 @@
"5503.9cb5f34b.chunk.css.map": "./static/css/5503.9cb5f34b.chunk.css.map",
"5503.8102c269.chunk.js.map": "./static/js/5503.8102c269.chunk.js.map",
"5926.a85e7cf5.chunk.js.map": "./static/js/5926.a85e7cf5.chunk.js.map",
"701.6d0fe16f.chunk.js.map": "./static/js/701.6d0fe16f.chunk.js.map",
"701.34df1a0d.chunk.js.map": "./static/js/701.34df1a0d.chunk.js.map",
"7821.461c5aa3.chunk.js.map": "./static/js/7821.461c5aa3.chunk.js.map",
"2850.9cb5f34b.chunk.css.map": "./static/css/2850.9cb5f34b.chunk.css.map",
"2850.c2c3a292.chunk.js.map": "./static/js/2850.c2c3a292.chunk.js.map",
@@ -212,6 +220,12 @@
"5517.9cb5f34b.chunk.css.map": "./static/css/5517.9cb5f34b.chunk.css.map",
"5517.730e0aeb.chunk.js.map": "./static/js/5517.730e0aeb.chunk.js.map",
"2555.47860755.chunk.js.map": "./static/js/2555.47860755.chunk.js.map",
"7486.83e0d248.chunk.js.map": "./static/js/7486.83e0d248.chunk.js.map",
"1377.6fbc40f3.chunk.js.map": "./static/js/1377.6fbc40f3.chunk.js.map",
"4672.7e50d90b.chunk.js.map": "./static/js/4672.7e50d90b.chunk.js.map",
"2516.5d118ef6.chunk.js.map": "./static/js/2516.5d118ef6.chunk.js.map",
"2759.3ab5d8ec.chunk.js.map": "./static/js/2759.3ab5d8ec.chunk.js.map",
"7295.c85034bf.chunk.js.map": "./static/js/7295.c85034bf.chunk.js.map",
"7585.cd3ad44f.chunk.js.map": "./static/js/7585.cd3ad44f.chunk.js.map",
"4902.b5c5ff07.chunk.js.map": "./static/js/4902.b5c5ff07.chunk.js.map",
"7847.659fc617.chunk.js.map": "./static/js/7847.659fc617.chunk.js.map",
@@ -233,9 +247,9 @@
"8833.9dccd2d5.chunk.js.map": "./static/js/8833.9dccd2d5.chunk.js.map",
"6526.e3612299.chunk.js.map": "./static/js/6526.e3612299.chunk.js.map",
"483.5b997456.chunk.js.map": "./static/js/483.5b997456.chunk.js.map",
"9467.42dc3ff9.chunk.js.map": "./static/js/9467.42dc3ff9.chunk.js.map",
"9467.8e1b707f.chunk.js.map": "./static/js/9467.8e1b707f.chunk.js.map",
"6895.488a4025.chunk.js.map": "./static/js/6895.488a4025.chunk.js.map",
"1379.3fee0dea.chunk.js.map": "./static/js/1379.3fee0dea.chunk.js.map",
"5882.41668ace.chunk.js.map": "./static/js/5882.41668ace.chunk.js.map",
"8277.e772e8bd.chunk.js.map": "./static/js/8277.e772e8bd.chunk.js.map",
"4133.2386eedc.chunk.js.map": "./static/js/4133.2386eedc.chunk.js.map",
"1367.9cb5f34b.chunk.css.map": "./static/css/1367.9cb5f34b.chunk.css.map",
@@ -276,8 +290,8 @@
"9179.73844de6.chunk.js.map": "./static/js/9179.73844de6.chunk.js.map",
"51.f63429fd.chunk.js.map": "./static/js/51.f63429fd.chunk.js.map",
"711.7013b9d7.chunk.js.map": "./static/js/711.7013b9d7.chunk.js.map",
"6901.e2472ebd.chunk.js.map": "./static/js/6901.e2472ebd.chunk.js.map",
"2185.164ae281.chunk.js.map": "./static/js/2185.164ae281.chunk.js.map",
"6901.cda3f1f9.chunk.js.map": "./static/js/6901.cda3f1f9.chunk.js.map",
"2185.4baca582.chunk.js.map": "./static/js/2185.4baca582.chunk.js.map",
"312.770148c8.chunk.js.map": "./static/js/312.770148c8.chunk.js.map",
"2112.c85537ec.chunk.js.map": "./static/js/2112.c85537ec.chunk.js.map",
"4619.bbf03503.chunk.js.map": "./static/js/4619.bbf03503.chunk.js.map",
@@ -323,10 +337,11 @@
"9515.a4e964be.chunk.js.map": "./static/js/9515.a4e964be.chunk.js.map",
"2983.e248775f.chunk.js.map": "./static/js/2983.e248775f.chunk.js.map",
"5861.65847210.chunk.js.map": "./static/js/5861.65847210.chunk.js.map",
"537.85347210.chunk.js.map": "./static/js/537.85347210.chunk.js.map",
"2763.08c6e1fd.chunk.js.map": "./static/js/2763.08c6e1fd.chunk.js.map"
},
"entrypoints": [
"static/css/main.b20a708b.css",
"static/js/main.ef5eea8b.js"
"static/js/main.2b781eaf.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><base href="/"/><meta content="width=device-width,initial-scale=1" name="viewport"/><meta content="#081C42" media="(prefers-color-scheme: light)" name="theme-color"/><meta content="#081C42" media="(prefers-color-scheme: dark)" name="theme-color"/><meta content="MinIO Console" name="description"/><link href="./styles/root-styles.css" rel="stylesheet"/><link href="./apple-icon-180x180.png" rel="apple-touch-icon" sizes="180x180"/><link href="./favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"/><link href="./favicon-96x96.png" rel="icon" sizes="96x96" type="image/png"/><link href="./favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"/><link href="./manifest.json" rel="manifest"/><link color="#3a4e54" href="./safari-pinned-tab.svg" rel="mask-icon"/><title>MinIO Console</title><script defer="defer" src="./static/js/main.ef5eea8b.js"></script><link href="./static/css/main.b20a708b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="preload"><img src="./images/background.svg"/> <img src="./images/background-wave-orig2.svg"/></div><div id="loader-block"><img src="./Loader.svg"/></div></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><base href="/"/><meta content="width=device-width,initial-scale=1" name="viewport"/><meta content="#081C42" media="(prefers-color-scheme: light)" name="theme-color"/><meta content="#081C42" media="(prefers-color-scheme: dark)" name="theme-color"/><meta content="MinIO Console" name="description"/><link href="./styles/root-styles.css" rel="stylesheet"/><link href="./apple-icon-180x180.png" rel="apple-touch-icon" sizes="180x180"/><link href="./favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"/><link href="./favicon-96x96.png" rel="icon" sizes="96x96" type="image/png"/><link href="./favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"/><link href="./manifest.json" rel="manifest"/><link color="#3a4e54" href="./safari-pinned-tab.svg" rel="mask-icon"/><title>MinIO Console</title><script defer="defer" src="./static/js/main.2b781eaf.js"></script><link href="./static/css/main.b20a708b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="preload"><img src="./images/background.svg"/> <img src="./images/background-wave-orig2.svg"/></div><div id="loader-block"><img src="./Loader.svg"/></div></div></body></html>

View File

@@ -1,2 +0,0 @@
"use strict";(self.webpackChunkportal_ui=self.webpackChunkportal_ui||[]).push([[1260],{1260:function(t,e,n){n.r(e);n(72791);var o=n(16871),a=n(25469),r=n(45248),u=n(81207),c=n(87995),l=n(46078),i=n(80184);e.default=function(){var t=(0,a.TL)(),e=(0,o.s0)();return function(){var n=function(){(0,r.Ov)(),t((0,c.wr)(!1)),localStorage.setItem("userLoggedIn",""),localStorage.setItem("redirect-path",""),t((0,l.lX)()),e("login")};u.Z.invoke("POST","/api/v1/logout").then((function(){n()})).catch((function(t){console.log(t),n()}))}(),(0,i.jsx)(i.Fragment,{})}}}]);
//# sourceMappingURL=1260.5248e62a.chunk.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"static/js/1260.5248e62a.chunk.js","mappings":"6MAmDA,UA1BmB,WACjB,IAAMA,GAAWC,EAAAA,EAAAA,MACXC,GAAWC,EAAAA,EAAAA,MAqBjB,OApBe,WACb,IAAMC,EAAgB,YACpBC,EAAAA,EAAAA,MACAL,GAASM,EAAAA,EAAAA,KAAW,IACpBC,aAAaC,QAAQ,eAAgB,IACrCD,aAAaC,QAAQ,gBAAiB,IACtCR,GAASS,EAAAA,EAAAA,OACTP,EAAS,QACV,EACDQ,EAAAA,EAAAA,OACU,OADV,kBAEGC,MAAK,WACJP,GACD,IACAQ,OAAM,SAACC,GACNC,QAAQC,IAAIF,GACZT,GACD,GACJ,CACDY,IACO,uBACR,C","sources":["screens/LogoutPage/LogoutPage.tsx"],"sourcesContent":["// This file is part of MinIO Console Server\n// Copyright (c) 2022 MinIO, Inc.\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <http://www.gnu.org/licenses/>.\n\nimport React from \"react\";\nimport { useNavigate } from \"react-router-dom\";\nimport { useAppDispatch } from \"../../store\";\nimport { ErrorResponseHandler } from \"../../common/types\";\nimport { clearSession } from \"../../common/utils\";\nimport api from \"../../common/api\";\nimport { userLogged } from \"../../systemSlice\";\nimport { resetSession } from \"../Console/consoleSlice\";\n\nconst LogoutPage = () => {\n const dispatch = useAppDispatch();\n const navigate = useNavigate();\n const logout = () => {\n const deleteSession = () => {\n clearSession();\n dispatch(userLogged(false));\n localStorage.setItem(\"userLoggedIn\", \"\");\n localStorage.setItem(\"redirect-path\", \"\");\n dispatch(resetSession());\n navigate(`login`);\n };\n api\n .invoke(\"POST\", `/api/v1/logout`)\n .then(() => {\n deleteSession();\n })\n .catch((err: ErrorResponseHandler) => {\n console.log(err);\n deleteSession();\n });\n };\n logout();\n return <></>;\n};\n\nexport default LogoutPage;\n"],"names":["dispatch","useAppDispatch","navigate","useNavigate","deleteSession","clearSession","userLogged","localStorage","setItem","resetSession","api","then","catch","err","console","log","logout"],"sourceRoot":""}

View File

@@ -0,0 +1,2 @@
"use strict";(self.webpackChunkportal_ui=self.webpackChunkportal_ui||[]).push([[1260],{1260:function(t,e,o){o.r(e);o(72791);var a=o(16871),n=o(25469),u=o(45248),c=o(87995),l=o(46078),r=o(81207),i=o(7241),s=o(80184);e.default=function(){var t=(0,n.TL)(),e=(0,a.s0)();return function(){var o=function(){(0,u.Ov)(),t((0,c.wr)(!1)),localStorage.setItem("userLoggedIn",""),localStorage.setItem("redirect-path",""),t((0,l.lX)()),e("/login")},a=localStorage.getItem("auth-state");r.Z.invoke("POST","/api/v1/logout",{state:a}).then((function(){o()})).catch((function(t){console.log(t),o()}))}(),(0,s.jsx)(i.Z,{})}}}]);
//# sourceMappingURL=1260.ded6e17b.chunk.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/js/1260.ded6e17b.chunk.js","mappings":"uNAqDA,UA3BmB,WACjB,IAAMA,GAAWC,EAAAA,EAAAA,MACXC,GAAWC,EAAAA,EAAAA,MAsBjB,OArBe,WACb,IAAMC,EAAgB,YACpBC,EAAAA,EAAAA,MACAL,GAASM,EAAAA,EAAAA,KAAW,IACpBC,aAAaC,QAAQ,eAAgB,IACrCD,aAAaC,QAAQ,gBAAiB,IACtCR,GAASS,EAAAA,EAAAA,OACTP,EAAS,SACV,EACKQ,EAAQH,aAAaI,QAAQ,cACnCC,EAAAA,EAAAA,OACU,OADV,iBACoC,CAAEF,MAAAA,IACnCG,MAAK,WACJT,GACD,IACAU,OAAM,SAACC,GACNC,QAAQC,IAAIF,GACZX,GACD,GACJ,CACDc,IACO,SAAC,IAAD,GACR,C","sources":["screens/LogoutPage/LogoutPage.tsx"],"sourcesContent":["// This file is part of MinIO Console Server\n// Copyright (c) 2022 MinIO, Inc.\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <http://www.gnu.org/licenses/>.\n\nimport React from \"react\";\nimport { useNavigate } from \"react-router-dom\";\nimport { useAppDispatch } from \"../../store\";\nimport { ErrorResponseHandler } from \"../../common/types\";\nimport { clearSession } from \"../../common/utils\";\nimport { userLogged } from \"../../systemSlice\";\nimport { resetSession } from \"../Console/consoleSlice\";\nimport api from \"../../common/api\";\nimport LoadingComponent from \"../../common/LoadingComponent\";\n\nconst LogoutPage = () => {\n const dispatch = useAppDispatch();\n const navigate = useNavigate();\n const logout = () => {\n const deleteSession = () => {\n clearSession();\n dispatch(userLogged(false));\n localStorage.setItem(\"userLoggedIn\", \"\");\n localStorage.setItem(\"redirect-path\", \"\");\n dispatch(resetSession());\n navigate(`/login`);\n };\n const state = localStorage.getItem(\"auth-state\");\n api\n .invoke(\"POST\", `/api/v1/logout`, { state })\n .then(() => {\n deleteSession();\n })\n .catch((err: ErrorResponseHandler) => {\n console.log(err);\n deleteSession();\n });\n };\n logout();\n return <LoadingComponent />;\n};\n\nexport default LogoutPage;\n"],"names":["dispatch","useAppDispatch","navigate","useNavigate","deleteSession","clearSession","userLogged","localStorage","setItem","resetSession","state","getItem","api","then","catch","err","console","log","logout"],"sourceRoot":""}

View File

@@ -0,0 +1,2 @@
"use strict";(self.webpackChunkportal_ui=self.webpackChunkportal_ui||[]).push([[1377],{81377:function(u,e,n){n.r(e);n(72791);var t=n(11135),r=n(25787),s=n(44959),i=n(80184);e.default=(0,r.Z)((function(u){return(0,t.Z)({})}))((function(u){u.classes;return(0,i.jsx)(s.Z,{idpType:"openid"})}))}}]);
//# sourceMappingURL=1377.6fbc40f3.chunk.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/js/1377.6fbc40f3.chunk.js","mappings":"6KAiCA,WAAeA,EAAAA,EAAAA,IANA,SAACC,GAAD,OAAkBC,EAAAA,EAAAA,GAAa,CAAC,EAAhC,GAMf,EAJgC,SAAC,GAA8C,EAA5CC,QACjC,OAAO,SAAC,IAAD,CAAmBC,QAAS,UACpC,G","sources":["screens/Console/IDP/IDPOpenIDConfigurations.tsx"],"sourcesContent":["// This file is part of MinIO Console Server\n// Copyright (c) 2022 MinIO, Inc.\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <http://www.gnu.org/licenses/>.\n\nimport React from \"react\";\n\nimport { Theme } from \"@mui/material/styles\";\nimport createStyles from \"@mui/styles/createStyles\";\nimport withStyles from \"@mui/styles/withStyles\";\nimport IDPConfigurations from \"./IDPConfigurations\";\n\ntype IDPOpenIDConfigurationsProps = {\n classes?: any;\n};\n\nconst styles = (theme: Theme) => createStyles({});\n\nconst IDPOpenIDConfigurations = ({ classes }: IDPOpenIDConfigurationsProps) => {\n return <IDPConfigurations idpType={\"openid\"} />;\n};\n\nexport default withStyles(styles)(IDPOpenIDConfigurations);\n"],"names":["withStyles","theme","createStyles","classes","idpType"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
"use strict";(self.webpackChunkportal_ui=self.webpackChunkportal_ui||[]).push([[2185],{62185:function(e,t,n){n.r(t);var a=n(29439),r=n(1413),i=n(72791),l=n(16871),s=n(75952),o=n(56028),c=n(61889),u=n(21435),d=n(11135),f=n(25787),h=n(23814),m=n(60364),x=n(45248),p=n(56096),j=n(25469),v=n(87995),b=n(80184),Z=(0,m.$j)((function(e){return{simplePath:e.objectBrowser.simplePath}}));t.default=Z((0,f.Z)((function(e){return(0,d.Z)((0,r.Z)((0,r.Z)({},h.ID),h.DF))}))((function(e){var t=e.modalOpen,n=(e.folderName,e.bucketName),r=e.onClose,d=e.classes,f=e.existingFiles,h=e.simplePath,m=(0,j.TL)(),Z=(0,l.s0)(),w=(0,i.useState)(""),P=(0,a.Z)(w,2),C=P[0],g=P[1],k=(0,i.useState)(!1),F=(0,a.Z)(k,2),N=F[0],y=F[1],E=(0,i.useState)(n),S=(0,a.Z)(E,2),z=S[0],B=S[1];(0,i.useEffect)((function(){if(h){var e="".concat(n).concat(n.endsWith("/")||h.startsWith("/")?"":"/").concat(h);B(e)}}),[h,n]);var I=function(){var e="/";h&&(e=h.endsWith("/")?h:"".concat(h,"/"));if(-1===f.findIndex((function(t){return t.name===e+C}))){var t=C.split("/").filter((function(e){return""!==e.trim()})).join("/"),a="/buckets/".concat(n,"/browse/").concat((0,x.LL)("".concat(e).concat(t,"/")));Z(a),r()}else m((0,v.zb)({errorMessage:"Folder cannot have the same name as an existing file",detailedError:""}))};(0,i.useEffect)((function(){var e=!0;0===C.trim().length&&(e=!1),y(e)}),[C]);return(0,b.jsx)(i.Fragment,{children:(0,b.jsx)(o.Z,{modalOpen:t,title:"Choose or create a new path",onClose:r,titleIcon:(0,b.jsx)(p.Z9m,{}),children:(0,b.jsxs)(c.ZP,{container:!0,children:[(0,b.jsxs)(c.ZP,{item:!0,xs:12,className:d.formFieldRow,children:[(0,b.jsx)("strong",{children:"Current Path:"})," ",(0,b.jsx)("br",{}),(0,b.jsx)("div",{style:{textOverflow:"ellipsis",whiteSpace:"nowrap",overflow:"hidden",fontSize:14,textAlign:"left"},dir:"rtl",children:z})]}),(0,b.jsx)(c.ZP,{item:!0,xs:12,className:d.formFieldRow,children:(0,b.jsx)(u.Z,{value:C,label:"New Folder Path",id:"folderPath",name:"folderPath",placeholder:"Enter the new Folder Path",onChange:function(e){g(e.target.value)},onKeyPress:function(e){"Enter"===e.code&&""!==C&&I()},required:!0})}),(0,b.jsxs)(c.ZP,{item:!0,xs:12,className:d.modalButtonBar,children:[(0,b.jsx)(s.zx,{id:"clear",type:"button",color:"primary",variant:"regular",onClick:function(){g("")},label:"Clear"}),(0,b.jsx)(s.zx,{id:"create",type:"submit",variant:"callAction",disabled:!N,onClick:I,label:"Create"})]})]})})})})))}}]);
//# sourceMappingURL=2185.164ae281.chunk.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
"use strict";(self.webpackChunkportal_ui=self.webpackChunkportal_ui||[]).push([[2185],{62185:function(e,t,n){n.r(t);var r=n(29439),a=n(1413),o=n(72791),i=n(16871),s=n(75952),l=n(56028),c=n(61889),u=n(21435),d=n(11135),f=n(25787),h=n(23814),m=n(60364),x=n(45248),p=n(56096),j=n(25469),v=n(87995),b=n(80184),w=(0,m.$j)((function(e){return{simplePath:e.objectBrowser.simplePath}}));t.default=w((0,f.Z)((function(e){return(0,d.Z)((0,a.Z)((0,a.Z)({},h.ID),h.DF))}))((function(e){var t=e.modalOpen,n=(e.folderName,e.bucketName),a=e.onClose,d=e.classes,f=e.simplePath,h=(0,j.TL)(),w=(0,i.s0)(),Z=(0,o.useState)(""),P=(0,r.Z)(Z,2),C=P[0],g=P[1],k=(0,o.useState)(!1),F=(0,r.Z)(k,2),N=F[0],y=F[1],E=(0,o.useState)(n),S=(0,r.Z)(E,2),z=S[0],B=S[1],I=(0,m.v9)((function(e){return e.objectBrowser.records}));(0,o.useEffect)((function(){if(f){var e="".concat(n).concat(n.endsWith("/")||f.startsWith("/")?"":"/").concat(f);B(e)}}),[f,n]);var L=function(){var e="/";f&&(e=f.endsWith("/")?f:"".concat(f,"/"));if(-1===I.findIndex((function(t){return t.name===e+C}))){var t=C.split("/").filter((function(e){return""!==e.trim()})).join("/"),r="/buckets/".concat(n,"/browse/").concat((0,x.LL)("".concat(e).concat(t,"/")));w(r),a()}else h((0,v.zb)({errorMessage:"Folder cannot have the same name as an existing file",detailedError:""}))};(0,o.useEffect)((function(){var e=!0;0===C.trim().length&&(e=!1),y(e)}),[C]);return(0,b.jsx)(o.Fragment,{children:(0,b.jsx)(l.Z,{modalOpen:t,title:"Choose or create a new path",onClose:a,titleIcon:(0,b.jsx)(p.Z9m,{}),children:(0,b.jsxs)(c.ZP,{container:!0,children:[(0,b.jsxs)(c.ZP,{item:!0,xs:12,className:d.formFieldRow,children:[(0,b.jsx)("strong",{children:"Current Path:"})," ",(0,b.jsx)("br",{}),(0,b.jsx)("div",{style:{textOverflow:"ellipsis",whiteSpace:"nowrap",overflow:"hidden",fontSize:14,textAlign:"left"},dir:"rtl",children:z})]}),(0,b.jsx)(c.ZP,{item:!0,xs:12,className:d.formFieldRow,children:(0,b.jsx)(u.Z,{value:C,label:"New Folder Path",id:"folderPath",name:"folderPath",placeholder:"Enter the new Folder Path",onChange:function(e){g(e.target.value)},onKeyPress:function(e){"Enter"===e.code&&""!==C&&L()},required:!0})}),(0,b.jsxs)(c.ZP,{item:!0,xs:12,className:d.modalButtonBar,children:[(0,b.jsx)(s.zx,{id:"clear",type:"button",color:"primary",variant:"regular",onClick:function(){g("")},label:"Clear"}),(0,b.jsx)(s.zx,{id:"create",type:"submit",variant:"callAction",disabled:!N,onClick:L,label:"Create"})]})]})})})})))}}]);
//# sourceMappingURL=2185.4baca582.chunk.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,2 @@
"use strict";(self.webpackChunkportal_ui=self.webpackChunkportal_ui||[]).push([[6914],{76914:function(e,t,a){a.r(t);var r=a(29439),o=a(72791),i=a(16871),n=a(81207),s=a(25787),l=a(11135),c=a(62666),d=a(10703),p=a(61889),g=a(20890),h=a(75952),m=a(15146),u=a(80184);t.default=(0,s.Z)((function(e){return(0,l.Z)({paper:{borderRadius:8,display:"flex",flexDirection:"column",alignItems:"center",width:800,height:424,margin:"auto",position:"absolute",top:"50%",left:"50%",marginLeft:-400,marginTop:-212,"&.MuiPaper-root":{borderRadius:8}},submit:{margin:"30px 0px 16px",height:40,boxShadow:"none",padding:"16px 30px"},mainContainer:{position:"relative",height:424},theOcean:{borderTopLeftRadius:8,borderBottomLeftRadius:8,background:"transparent linear-gradient(to bottom, #073052 0%,#05122b 100%); 0% 0% no-repeat padding-box;"},oceanBg:{backgroundImage:"url(/images/BG_Illustration.svg)",backgroundRepeat:"no-repeat",backgroundPosition:"bottom left",height:"100%",width:324},theLogin:{padding:"40px 45px 20px 45px"},extraDetailsContainer:{fontStyle:"italic",color:"#9C9C9C",transition:"all .2s ease-in-out",padding:"0 5px",marginTop:5,overflow:"auto"},errorLabel:{color:"#000",fontSize:18,fontWeight:500,marginLeft:5},simpleError:{marginTop:5,padding:"2px 5px",fontSize:16,color:"#000"},messageIcon:{color:"#C72C48",display:"flex","& svg":{width:32,height:32}},errorTitle:{display:"flex",alignItems:"center"}})}))((function(e){var t=e.classes,a=(0,i.s0)(),s=(0,o.useState)(""),l=(0,r.Z)(s,2),x=l[0],f=l[1],b=(0,o.useState)(""),v=(0,r.Z)(b,2),j=v[0],C=v[1],S=(0,o.useState)(!0),Z=(0,r.Z)(S,2),k=Z[0],w=Z[1];return(0,o.useEffect)((function(){if(k){var e=window.location.search,t=new URLSearchParams(e),r=t.get("code"),o=t.get("state"),i=t.get("error"),s=t.get("errorDescription");i||s?(f(i||""),C(s||""),w(!1)):n.Z.invoke("POST","/api/v1/login/oauth2/auth",{code:r,state:o}).then((function(){var e="/";localStorage.getItem("redirect-path")&&""!==localStorage.getItem("redirect-path")&&(e="".concat(localStorage.getItem("redirect-path")),localStorage.setItem("redirect-path","")),w(!1),a(e)})).catch((function(e){f(e.errorMessage),C(e.detailedError),w(!1)}))}}),[k,a]),""!==x||""!==j?(0,u.jsx)(o.Fragment,{children:(0,u.jsx)(d.Z,{className:t.paper,children:(0,u.jsxs)(p.ZP,{container:!0,className:t.mainContainer,children:[(0,u.jsx)(p.ZP,{item:!0,xs:7,className:t.theOcean,children:(0,u.jsx)("div",{className:t.oceanBg})}),(0,u.jsxs)(p.ZP,{item:!0,xs:5,className:t.theLogin,children:[(0,u.jsxs)("div",{className:t.errorTitle,children:[(0,u.jsx)("span",{className:t.messageIcon,children:(0,u.jsx)(m.Z,{})}),(0,u.jsx)("span",{className:t.errorLabel,children:"Error from IDP"})]}),(0,u.jsx)("div",{className:t.simpleError,children:x}),(0,u.jsx)(g.Z,{variant:"body1",gutterBottom:!0,component:"div",className:t.extraDetailsContainer,children:j}),(0,u.jsx)(h.zx,{id:"back-to-login",onClick:function(){window.location.href="".concat(c.F,"login")},type:"submit",variant:"callAction",fullWidth:!0,children:"Back to Login"})]})]})})}):null}))},15146:function(e,t,a){var r=a(64836);t.Z=void 0;var o=r(a(45649)),i=a(80184),n=(0,o.default)((0,i.jsx)("path",{d:"M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"}),"ErrorOutline");t.Z=n}}]);
//# sourceMappingURL=6914.8c73a010.chunk.js.map
"use strict";(self.webpackChunkportal_ui=self.webpackChunkportal_ui||[]).push([[6914],{76914:function(e,t,a){a.r(t);var r=a(29439),o=a(72791),i=a(16871),n=a(81207),s=a(25787),l=a(11135),c=a(62666),d=a(10703),g=a(61889),p=a(20890),h=a(75952),m=a(15146),u=a(80184);t.default=(0,s.Z)((function(e){return(0,l.Z)({paper:{borderRadius:8,display:"flex",flexDirection:"column",alignItems:"center",width:800,height:424,margin:"auto",position:"absolute",top:"50%",left:"50%",marginLeft:-400,marginTop:-212,"&.MuiPaper-root":{borderRadius:8}},submit:{margin:"30px 0px 16px",height:40,boxShadow:"none",padding:"16px 30px"},mainContainer:{position:"relative",height:424},theOcean:{borderTopLeftRadius:8,borderBottomLeftRadius:8,background:"transparent linear-gradient(to bottom, #073052 0%,#05122b 100%); 0% 0% no-repeat padding-box;"},oceanBg:{backgroundImage:"url(/images/BG_Illustration.svg)",backgroundRepeat:"no-repeat",backgroundPosition:"bottom left",height:"100%",width:324},theLogin:{padding:"40px 45px 20px 45px"},extraDetailsContainer:{fontStyle:"italic",color:"#9C9C9C",transition:"all .2s ease-in-out",padding:"0 5px",marginTop:5,overflow:"auto"},errorLabel:{color:"#000",fontSize:18,fontWeight:500,marginLeft:5},simpleError:{marginTop:5,padding:"2px 5px",fontSize:16,color:"#000"},messageIcon:{color:"#C72C48",display:"flex","& svg":{width:32,height:32}},errorTitle:{display:"flex",alignItems:"center"}})}))((function(e){var t=e.classes,a=(0,i.s0)(),s=(0,o.useState)(""),l=(0,r.Z)(s,2),x=l[0],f=l[1],b=(0,o.useState)(""),v=(0,r.Z)(b,2),S=v[0],j=v[1],C=(0,o.useState)(!0),Z=(0,r.Z)(C,2),k=Z[0],w=Z[1];return(0,o.useEffect)((function(){if(k){var e=window.location.search,t=new URLSearchParams(e),r=t.get("code"),o=t.get("state"),i=t.get("error"),s=t.get("errorDescription");i||s?(f(i||""),j(s||""),w(!1)):n.Z.invoke("POST","/api/v1/login/oauth2/auth",{code:r,state:o}).then((function(){var e="/";localStorage.getItem("redirect-path")&&""!==localStorage.getItem("redirect-path")&&(e="".concat(localStorage.getItem("redirect-path")),localStorage.setItem("redirect-path","")),o&&localStorage.setItem("auth-state",o),w(!1),a(e)})).catch((function(e){f(e.errorMessage),j(e.detailedError),w(!1)}))}}),[k,a]),""!==x||""!==S?(0,u.jsx)(o.Fragment,{children:(0,u.jsx)(d.Z,{className:t.paper,children:(0,u.jsxs)(g.ZP,{container:!0,className:t.mainContainer,children:[(0,u.jsx)(g.ZP,{item:!0,xs:7,className:t.theOcean,children:(0,u.jsx)("div",{className:t.oceanBg})}),(0,u.jsxs)(g.ZP,{item:!0,xs:5,className:t.theLogin,children:[(0,u.jsxs)("div",{className:t.errorTitle,children:[(0,u.jsx)("span",{className:t.messageIcon,children:(0,u.jsx)(m.Z,{})}),(0,u.jsx)("span",{className:t.errorLabel,children:"Error from IDP"})]}),(0,u.jsx)("div",{className:t.simpleError,children:x}),(0,u.jsx)(p.Z,{variant:"body1",gutterBottom:!0,component:"div",className:t.extraDetailsContainer,children:S}),(0,u.jsx)(h.zx,{id:"back-to-login",onClick:function(){window.location.href="".concat(c.F,"login")},type:"submit",variant:"callAction",fullWidth:!0,children:"Back to Login"})]})]})})}):null}))},15146:function(e,t,a){var r=a(64836);t.Z=void 0;var o=r(a(45649)),i=a(80184),n=(0,o.default)((0,i.jsx)("path",{d:"M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"}),"ErrorOutline");t.Z=n}}]);
//# sourceMappingURL=6914.1699f207.chunk.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
"use strict";(self.webpackChunkportal_ui=self.webpackChunkportal_ui||[]).push([[7486],{17486:function(u,e,n){n.r(e);n(72791);var t=n(11135),r=n(25787),s=n(44959),a=n(80184);e.default=(0,r.Z)((function(u){return(0,t.Z)({})}))((function(u){u.classes;return(0,a.jsx)(s.Z,{idpType:"ldap"})}))}}]);
//# sourceMappingURL=7486.83e0d248.chunk.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"static/js/7486.83e0d248.chunk.js","mappings":"6KAiCA,WAAeA,EAAAA,EAAAA,IANA,SAACC,GAAD,OAAkBC,EAAAA,EAAAA,GAAa,CAAC,EAAhC,GAMf,EAJ8B,SAAC,GAA4C,EAA1CC,QAC/B,OAAO,SAAC,IAAD,CAAmBC,QAAS,QACpC,G","sources":["screens/Console/IDP/IDPLDAPConfigurations.tsx"],"sourcesContent":["// This file is part of MinIO Console Server\n// Copyright (c) 2022 MinIO, Inc.\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <http://www.gnu.org/licenses/>.\n\nimport React from \"react\";\n\nimport { Theme } from \"@mui/material/styles\";\nimport createStyles from \"@mui/styles/createStyles\";\nimport withStyles from \"@mui/styles/withStyles\";\nimport IDPConfigurations from \"./IDPConfigurations\";\n\ntype IDPLDAPConfigurationsProps = {\n classes?: any;\n};\n\nconst styles = (theme: Theme) => createStyles({});\n\nconst IDPLDAPConfigurations = ({ classes }: IDPLDAPConfigurationsProps) => {\n return <IDPConfigurations idpType={\"ldap\"} />;\n};\n\nexport default withStyles(styles)(IDPLDAPConfigurations);\n"],"names":["withStyles","theme","createStyles","classes","idpType"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

View File

@@ -30,7 +30,7 @@
"kbar": "^0.1.0-beta.34",
"local-storage-fallback": "^4.1.1",
"lodash": "^4.17.21",
"mds": "https://github.com/minio/mds.git#v0.0.5",
"mds": "https://github.com/minio/mds.git#v0.0.7",
"minio": "^7.0.28",
"moment": "^2.29.4",
"react": "^18.1.0",

View File

@@ -39,7 +39,14 @@ const MainRouter = () => {
</Suspense>
}
/>
<Route path="/logout" element={<Logout />} />
<Route
path="/logout"
element={
<Suspense fallback={<LoadingComponent />}>
<Logout />
</Suspense>
}
/>
<Route
path="/login"
element={

View File

@@ -35,6 +35,7 @@ import { SRInfoStateType } from "./types";
import { AppState, useAppDispatch } from "./store";
import { saveSessionResponse } from "./screens/Console/consoleSlice";
import { getOverrideColorVariants } from "./utils/stylesUtils";
import LoadingComponent from "./common/LoadingComponent";
interface ProtectedRouteProps {
Component: any;
@@ -119,7 +120,7 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
// if we're still trying to retrieve user session render nothing
if (sessionLoading) {
return null;
return <LoadingComponent />;
}
// redirect user to the right page based on session status
return userLoggedIn ? <Component /> : <StorePathAndRedirect />;

View File

@@ -30,8 +30,6 @@ const LoadingComponent = () => {
>
<Grid item xs={3} style={{ textAlign: "center" }}>
<Loader style={{ width: 35, height: 35 }} />
<br />
Loading...
</Grid>
</Grid>
);

View File

@@ -130,6 +130,15 @@ export const IAM_PAGES = {
ACCOUNT_ADD: "/access-keys/new-account",
USER_SA_ACCOUNT_ADD: "/identity/users/new-user-sa/:userName",
/* IDP */
IDP_LDAP_CONFIGURATIONS: "/idp/ldap/configurations",
IDP_LDAP_CONFIGURATIONS_VIEW: "/idp/ldap/configurations/:idpName",
IDP_LDAP_CONFIGURATIONS_ADD: "/idp/ldap/configurations/add-idp",
IDP_OPENID_CONFIGURATIONS: "/idp/openid/configurations",
IDP_OPENID_CONFIGURATIONS_VIEW: "/idp/openid/configurations/:idpName",
IDP_OPENID_CONFIGURATIONS_ADD: "/idp/openid/configurations/add-idp",
POLICIES: "/identity/policies",
POLICY_ADD: "/identity/add-policy",
POLICIES_VIEW: "/identity/policies/*",
@@ -430,6 +439,30 @@ export const IAM_PAGES_PERMISSIONS = {
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.IDP_LDAP_CONFIGURATIONS]: [
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.IDP_LDAP_CONFIGURATIONS_ADD]: [
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.IDP_LDAP_CONFIGURATIONS_VIEW]: [
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.IDP_OPENID_CONFIGURATIONS]: [
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.IDP_OPENID_CONFIGURATIONS_ADD]: [
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.IDP_OPENID_CONFIGURATIONS_VIEW]: [
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
};
export const S3_ALL_RESOURCES = "arn:aws:s3:::*";

View File

@@ -84,7 +84,9 @@ export const deleteCookie = (name: string) => {
export const clearSession = () => {
storage.removeItem("token");
storage.removeItem("auth-state");
deleteCookie("token");
deleteCookie("idp-refresh-token");
};
// timeFromDate gets time string from date input

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, { Fragment, useEffect } from "react";
import React, { Fragment, useCallback, useEffect } from "react";
import { useSelector } from "react-redux";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { Theme } from "@mui/material/styles";
@@ -37,8 +37,22 @@ import {
} from "../../../../common/SecureComponent/permissions";
import BackLink from "../../../../common/BackLink";
import {
newMessage,
resetMessages,
setIsVersioned,
setLoadingLocking,
setLoadingObjectInfo,
setLoadingObjects,
setLoadingRecords,
setLoadingVersioning,
setLoadingVersions,
setLockingEnabled,
setObjectDetailsView,
setRecords,
setSearchObjects,
setSearchVersions,
setSelectedObjectView,
setSimplePathHandler,
setVersionsModeEnabled,
} from "../../ObjectBrowser/objectBrowserSlice";
import SearchBox from "../../Common/SearchBox";
@@ -47,18 +61,99 @@ import AutoColorIcon from "../../Common/Components/AutoColorIcon";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import { Button } from "mds";
import hasPermission from "../../../../common/SecureComponent/accessControl";
import { IMessageEvent } from "websocket";
import { wsProtocol } from "../../../../utils/wsUtils";
import {
WebsocketRequest,
WebsocketResponse,
} from "../ListBuckets/Objects/ListObjects/types";
import { decodeURLString, encodeURLString } from "../../../../common/utils";
import { permissionItems } from "../ListBuckets/Objects/utils";
import { setErrorSnackMessage } from "../../../../systemSlice";
import api from "../../../../common/api";
import { BucketObjectLocking, BucketVersioning } from "../types";
import { ErrorResponseHandler } from "../../../../common/types";
const styles = (theme: Theme) =>
createStyles({
...containerForHeader(theme.spacing(4)),
});
let objectsWS: WebSocket;
let currentRequestID: number = 0;
let errorCounter: number = 0;
let wsInFlight: boolean = false;
const initWSConnection = (
openCallback?: () => void,
onMessageCallback?: (message: IMessageEvent) => void
) => {
if (wsInFlight) {
return;
}
wsInFlight = true;
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
// check if we are using base path, if not this always is `/`
const baseLocation = new URL(document.baseURI);
const baseUrl = baseLocation.pathname;
const wsProt = wsProtocol(url.protocol);
objectsWS = new WebSocket(
`${wsProt}://${url.hostname}:${port}${baseUrl}ws/objectManager`
);
objectsWS.onopen = () => {
wsInFlight = false;
if (openCallback) {
openCallback();
}
errorCounter = 0;
};
if (onMessageCallback) {
objectsWS.onmessage = onMessageCallback;
}
const reconnectFn = () => {
if (errorCounter <= 5) {
initWSConnection(openCallback, onMessageCallback);
errorCounter += 1;
} else {
console.error("Websocket not available.");
}
};
objectsWS.onclose = () => {
wsInFlight = false;
console.warn("Websocket Disconnected. Attempting Reconnection...");
// We reconnect after 3 seconds
setTimeout(reconnectFn, 3000);
};
objectsWS.onerror = () => {
wsInFlight = false;
console.error("Error in websocket connection. Attempting reconnection...");
// We reconnect after 3 seconds
setTimeout(reconnectFn, 3000);
};
};
const BrowserHandler = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const params = useParams();
const location = useLocation();
const loadingVersioning = useSelector(
(state: AppState) => state.objectBrowser.loadingVersioning
);
const versionsMode = useSelector(
(state: AppState) => state.objectBrowser.versionsMode
);
@@ -71,6 +166,36 @@ const BrowserHandler = () => {
const searchVersions = useSelector(
(state: AppState) => state.objectBrowser.searchVersions
);
const rewindEnabled = useSelector(
(state: AppState) => state.objectBrowser.rewind.rewindEnabled
);
const rewindDate = useSelector(
(state: AppState) => state.objectBrowser.rewind.dateToRewind
);
const showDeleted = useSelector(
(state: AppState) => state.objectBrowser.showDeleted
);
const allowResources = useSelector(
(state: AppState) => state.console.session.allowResources
);
const loadingObjects = useSelector(
(state: AppState) => state.objectBrowser.loadingObjects
);
const loadingLocking = useSelector(
(state: AppState) => state.objectBrowser.loadingLocking
);
const bucketToRewind = useSelector(
(state: AppState) => state.objectBrowser.rewind.bucketToRewind
);
const loadRecords = useSelector(
(state: AppState) => state.objectBrowser.loadRecords
);
const detailsOpen = useSelector(
(state: AppState) => state.objectBrowser.objectDetailsOpen
);
const selectedInternalPaths = useSelector(
(state: AppState) => state.objectBrowser.selectedInternalPaths
);
const features = useSelector(selFeatures);
@@ -81,10 +206,286 @@ const BrowserHandler = () => {
const obOnly = !!features?.includes("object-browser-only");
/*WS Request Handlers*/
const onMessageCallBack = useCallback(
(message: IMessageEvent) => {
// reset start status
dispatch(setLoadingObjects(false));
const response: WebsocketResponse = JSON.parse(message.data.toString());
if (currentRequestID === response.request_id) {
// If response is not from current request, we can omit
if (response.request_id !== currentRequestID) {
return;
}
if (
response.error ===
"The Access Key Id you provided does not exist in our records."
) {
// Session expired.
window.location.reload();
} else if (response.error === "Access Denied.") {
let pathPrefix = "";
if (internalPaths) {
const decodedPath = decodeURLString(internalPaths);
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
const permitItems = permissionItems(
bucketName,
pathPrefix,
allowResources || []
);
if (!permitItems || permitItems.length === 0) {
dispatch(
setErrorSnackMessage({
errorMessage: response.error,
detailedError: response.error,
})
);
} else {
dispatch(setRecords(permitItems));
}
return;
}
// This indicates final messages is received.
if (response.request_end) {
dispatch(setLoadingObjects(false));
dispatch(setLoadingRecords(false));
return;
}
if (response.data) {
dispatch(newMessage(response.data));
}
}
},
[dispatch, internalPaths, allowResources, bucketName]
);
const initWSRequest = useCallback(
(path: string, date: Date) => {
if (objectsWS && objectsWS.readyState === 1) {
try {
const newRequestID = currentRequestID + 1;
dispatch(resetMessages());
const request: WebsocketRequest = {
bucket_name: bucketName,
prefix: encodeURLString(path),
mode: rewindEnabled || showDeleted ? "rewind" : "objects",
date: date.toISOString(),
request_id: newRequestID,
};
objectsWS.send(JSON.stringify(request));
// We store the new ID for the requestID
currentRequestID = newRequestID;
} catch (e) {
console.error(e);
}
} else {
// Socket is disconnected, we request reconnection but will need to recreate call
const dupRequest = () => {
initWSRequest(path, date);
};
initWSConnection(dupRequest, onMessageCallBack);
}
},
[bucketName, rewindEnabled, showDeleted, dispatch, onMessageCallBack]
);
useEffect(() => {
return () => {
const request: WebsocketRequest = {
mode: "cancel",
request_id: currentRequestID,
};
if (objectsWS && objectsWS.readyState === 1) {
objectsWS.send(JSON.stringify(request));
}
};
}, []);
useEffect(() => {
if (objectsWS?.readyState === 1) {
const decodedIPaths = decodeURLString(internalPaths);
if (decodedIPaths.endsWith("/") || decodedIPaths === "") {
dispatch(setObjectDetailsView(false));
dispatch(setSelectedObjectView(null));
dispatch(
setSimplePathHandler(decodedIPaths === "" ? "/" : decodedIPaths)
);
} else {
dispatch(setLoadingObjectInfo(true));
dispatch(setObjectDetailsView(true));
dispatch(setLoadingVersions(true));
dispatch(
setSelectedObjectView(
`${decodedIPaths ? `${encodeURLString(decodedIPaths)}` : ``}`
)
);
dispatch(
setSimplePathHandler(
`${decodedIPaths.split("/").slice(0, -1).join("/")}/`
)
);
}
}
}, [internalPaths, rewindDate, rewindEnabled, dispatch]);
// Direct file access effect / prefix
useEffect(() => {
if (!loadingObjects && loadRecords && !rewindEnabled) {
const parentPath = `${decodeURLString(internalPaths)
.split("/")
.slice(0, -1)
.join("/")}/`;
initWSRequest(parentPath, new Date());
}
}, [
loadingObjects,
loadRecords,
bucketName,
bucketToRewind,
dispatch,
internalPaths,
rewindDate,
rewindEnabled,
initWSRequest,
detailsOpen,
]);
const displayListObjects = hasPermission(bucketName, [
IAM_SCOPES.S3_LIST_BUCKET,
]);
// Common objects list
useEffect(() => {
// begin watch if bucketName in bucketList and start pressed
if (loadingObjects && displayListObjects) {
let pathPrefix = "";
if (internalPaths) {
const decodedPath = decodeURLString(internalPaths);
// internalPaths are selected (file details), we split and get parent folder
if (selectedInternalPaths === internalPaths) {
pathPrefix = `${decodeURLString(internalPaths)
.split("/")
.slice(0, -1)
.join("/")}/`;
} else {
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
}
let requestDate = new Date();
if (rewindEnabled && rewindDate) {
requestDate = rewindDate;
}
initWSRequest(pathPrefix, requestDate);
} else {
dispatch(setLoadingObjects(false));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
loadingObjects,
internalPaths,
dispatch,
rewindDate,
rewindEnabled,
displayListObjects,
initWSRequest,
]);
useEffect(() => {
dispatch(setVersionsModeEnabled({ status: false }));
}, [internalPaths, dispatch]);
useEffect(() => {
if (loadingVersioning) {
if (displayListObjects) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/versioning`)
.then((res: BucketVersioning) => {
dispatch(setIsVersioned(res.is_versioned));
dispatch(setLoadingVersioning(false));
})
.catch((err: ErrorResponseHandler) => {
console.error(
"Error Getting Object Versioning Status: ",
err.detailedError
);
dispatch(setLoadingVersioning(false));
});
} else {
dispatch(setLoadingVersioning(false));
dispatch(resetMessages());
}
}
}, [bucketName, loadingVersioning, dispatch, displayListObjects]);
useEffect(() => {
if (loadingLocking) {
if (displayListObjects) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/object-locking`)
.then((res: BucketObjectLocking) => {
dispatch(setLockingEnabled(res.object_locking_enabled));
dispatch(setLoadingLocking(false));
})
.catch((err: ErrorResponseHandler) => {
console.error(
"Error Getting Object Locking Status: ",
err.detailedError
);
dispatch(setLoadingLocking(false));
});
} else {
dispatch(resetMessages());
dispatch(setLoadingLocking(false));
}
}
}, [bucketName, loadingLocking, dispatch, displayListObjects]);
useEffect(() => {
if (loadingLocking) {
if (displayListObjects) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/object-locking`)
.then((res: BucketObjectLocking) => {
dispatch(setLockingEnabled(res.object_locking_enabled));
setLoadingLocking(false);
})
.catch((err: ErrorResponseHandler) => {
console.error(
"Error Getting Object Locking Status: ",
err.detailedError
);
setLoadingLocking(false);
});
} else {
dispatch(resetMessages());
setLoadingLocking(false);
}
}
}, [bucketName, loadingLocking, dispatch, displayListObjects]);
const openBucketConfiguration = () => {
navigate(`/buckets/${bucketName}/admin`);
};

View File

@@ -64,6 +64,7 @@ import { selFeatures } from "../../consoleSlice";
import AutoColorIcon from "../../Common/Components/AutoColorIcon";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import AButton from "../../Common/AButton/AButton";
import { setLoadingObjects } from "../../ObjectBrowser/objectBrowserSlice";
const styles = (theme: Theme) =>
createStyles({
@@ -123,6 +124,7 @@ const ListBuckets = ({ classes }: IListBucketsProps) => {
.then((res: BucketList) => {
setLoading(false);
setRecords(res.buckets || []);
dispatch(setLoadingObjects(true));
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);

View File

@@ -27,7 +27,7 @@ import {
formFieldStyles,
modalStyleUtils,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { connect } from "react-redux";
import { connect, useSelector } from "react-redux";
import { encodeURLString } from "../../../../../../common/utils";
import { BucketObjectItem } from "./types";
@@ -41,7 +41,6 @@ interface ICreatePath {
bucketName: string;
folderName: string;
onClose: () => any;
existingFiles: BucketObjectItem[];
simplePath: string | null;
}
@@ -57,7 +56,6 @@ const CreatePathModal = ({
bucketName,
onClose,
classes,
existingFiles,
simplePath,
}: ICreatePath) => {
const dispatch = useAppDispatch();
@@ -67,6 +65,8 @@ const CreatePathModal = ({
const [isFormValid, setIsFormValid] = useState<boolean>(false);
const [currentPath, setCurrentPath] = useState(bucketName);
const records = useSelector((state: AppState) => state.objectBrowser.records);
useEffect(() => {
if (simplePath) {
const newPath = `${bucketName}${
@@ -91,7 +91,7 @@ const CreatePathModal = ({
const sharesName = (record: BucketObjectItem) =>
record.name === folderPath + pathUrl;
if (existingFiles.findIndex(sharesName) !== -1) {
if (records.findIndex(sharesName) !== -1) {
dispatch(
setModalErrorSnackMessage({
errorMessage: "Folder cannot have the same name as an existing file",

View File

@@ -30,15 +30,10 @@ import { Button } from "mds";
import createStyles from "@mui/styles/createStyles";
import Grid from "@mui/material/Grid";
import get from "lodash/get";
import { BucketObjectItem, BucketObjectItemsList } from "./types";
import api from "../../../../../../common/api";
import TableWrapper, {
ItemActions,
} from "../../../../Common/TableWrapper/TableWrapper";
import {
decodeURLString,
encodeURLString,
getClientOS,
niceBytesInt,
} from "../../../../../../common/utils";
@@ -50,27 +45,16 @@ import {
searchField,
tableStyles,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { Badge, Typography } from "@mui/material";
import { Badge } from "@mui/material";
import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs";
import {
download,
extensionPreview,
permissionItems,
sortListObjects,
} from "../utils";
import {
BucketInfo,
BucketObjectLocking,
BucketQuota,
BucketVersioning,
} from "../../../types";
import { extensionPreview } from "../utils";
import { BucketInfo, BucketQuota } from "../../../types";
import { ErrorResponseHandler } from "../../../../../../common/types";
import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle";
import { AppState, useAppDispatch } from "../../../../../../store";
import PageLayout from "../../../../Common/Layout/PageLayout";
import {
IAM_SCOPES,
permissionTooltipHelper,
@@ -79,7 +63,6 @@ import {
hasPermission,
SecureComponent,
} from "../../../../../../common/SecureComponent";
import withSuspense from "../../../../Common/Components/withSuspense";
import {
BucketsIcon,
@@ -91,7 +74,6 @@ import UploadFilesButton from "../../UploadFilesButton";
import DetailsListPanel from "./DetailsListPanel";
import ObjectDetailPanel from "./ObjectDetailPanel";
import ActionsListSection from "./ActionsListSection";
import { listModeColumns, rewindModeColumns } from "./ListObjectsHelpers";
import VersionsNavigator from "../ObjectDetails/VersionsNavigator";
import CheckboxWrapper from "../../../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
@@ -111,16 +93,21 @@ import {
completeObject,
failObject,
openList,
resetMessages,
resetRewind,
setLoadingObjectInfo,
setLoadingObjectsList,
setDownloadRenameModal,
setLoadingObjects,
setLoadingRecords,
setLoadingVersions,
setNewObject,
setObjectDetailsView,
setPreviewOpen,
setSearchObjects,
setSelectedObjects,
setSelectedObjectView,
setSelectedPreview,
setShareFileModalOpen,
setShowDeletedObjects,
setSimplePathHandler,
setVersionsModeEnabled,
updateProgress,
} from "../../../../ObjectBrowser/objectBrowserSlice";
@@ -132,8 +119,13 @@ import {
setBucketInfo,
} from "../../../BucketDetails/bucketDetailsSlice";
import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename";
import { selFeatures } from "../../../../consoleSlice";
import TooltipWrapper from "../../../../Common/TooltipWrapper/TooltipWrapper";
import ListObjectsTable from "./ListObjectsTable";
import {
downloadSelected,
openPreview,
openShare,
} from "../../../../ObjectBrowser/objectBrowserThunks";
const HistoryIcon = React.lazy(
() => import("../../../../../../icons/HistoryIcon")
@@ -159,28 +151,6 @@ const PreviewFileModal = withSuspense(
const useStyles = makeStyles((theme: Theme) =>
createStyles({
browsePaper: {
border: 0,
height: "calc(100vh - 210px)",
"&.isEmbedded": {
height: "calc(100vh - 315px)",
},
"&.actionsPanelOpen": {
minHeight: "100%",
},
"@media (max-width: 800px)": {
width: 800,
},
},
"@global": {
".rowLine:hover .iconFileElm": {
backgroundImage: "url(/images/ob_file_filled.svg)",
},
".rowLine:hover .iconFolderElm": {
backgroundImage: "url(/images/ob_folder_filled.svg)",
},
},
badgeOverlap: {
"& .MuiBadge-badge": {
top: 10,
@@ -215,12 +185,8 @@ const useStyles = makeStyles((theme: Theme) =>
breadcrumbsContainer: {
padding: "12px 14px 5px",
},
parentWrapper: {
"@media (max-width: 800px)": {
overflowX: "auto",
},
},
fullContainer: {
position: "relative",
"@media (max-width: 799px)": {
width: 0,
},
@@ -255,31 +221,6 @@ const acceptDnDStyle = {
borderColor: "#00e676",
};
function useInterval(callback: any, delay: number) {
const savedCallback = useRef<Function | null>(null);
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
if (savedCallback !== undefined && savedCallback.current) {
savedCallback.current();
}
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
const defLoading = <Typography component="h3">Loading...</Typography>;
const ListObjects = () => {
const classes = useStyles();
const dispatch = useAppDispatch();
@@ -290,9 +231,6 @@ const ListObjects = () => {
const rewindEnabled = useSelector(
(state: AppState) => state.objectBrowser.rewind.rewindEnabled
);
const rewindDate = useSelector(
(state: AppState) => state.objectBrowser.rewind.dateToRewind
);
const bucketToRewind = useSelector(
(state: AppState) => state.objectBrowser.rewind.bucketToRewind
);
@@ -300,9 +238,6 @@ const ListObjects = () => {
(state: AppState) => state.objectBrowser.versionsMode
);
const searchObjects = useSelector(
(state: AppState) => state.objectBrowser.searchObjects
);
const showDeleted = useSelector(
(state: AppState) => state.objectBrowser.showDeleted
);
@@ -312,47 +247,41 @@ const ListObjects = () => {
const selectedInternalPaths = useSelector(
(state: AppState) => state.objectBrowser.selectedInternalPaths
);
const loading = useSelector(
const loadingObjects = useSelector(
(state: AppState) => state.objectBrowser.loadingObjects
);
const simplePath = useSelector(
(state: AppState) => state.objectBrowser.simplePath
);
const loadingBucket = useSelector(selBucketDetailsLoading);
const bucketInfo = useSelector(selBucketDetailsInfo);
const allowResources = useSelector(
(state: AppState) => state.console.session.allowResources
const isVersioned = useSelector(
(state: AppState) => state.objectBrowser.isVersioned
);
const lockingEnabled = useSelector(
(state: AppState) => state.objectBrowser.lockingEnabled
);
const downloadRenameModal = useSelector(
(state: AppState) => state.objectBrowser.downloadRenameModal
);
const selectedPreview = useSelector(
(state: AppState) => state.objectBrowser.selectedPreview
);
const shareFileModalOpen = useSelector(
(state: AppState) => state.objectBrowser.shareFileModalOpen
);
const previewOpen = useSelector(
(state: AppState) => state.objectBrowser.previewOpen
);
const features = useSelector(selFeatures);
const obOnly = !!features?.includes("object-browser-only");
const loadingBucket = useSelector(selBucketDetailsLoading);
const bucketInfo = useSelector(selBucketDetailsInfo);
const [records, setRecords] = useState<BucketObjectItem[]>([]);
const [deleteMultipleOpen, setDeleteMultipleOpen] = useState<boolean>(false);
const [loadingStartTime, setLoadingStartTime] = useState<number>(0);
const [loadingMessage, setLoadingMessage] =
useState<React.ReactNode>(defLoading);
const [loadingVersioning, setLoadingVersioning] = useState<boolean>(true);
const [isVersioned, setIsVersioned] = useState<boolean>(false);
const [loadingLocking, setLoadingLocking] = useState<boolean>(true);
const [lockingEnabled, setLockingEnabled] = useState<boolean>(false);
const [rewindSelect, setRewindSelect] = useState<boolean>(false);
const [selectedObjects, setSelectedObjects] = useState<string[]>([]);
const [previewOpen, setPreviewOpen] = useState<boolean>(false);
const [selectedPreview, setSelectedPreview] =
useState<BucketObjectItem | null>(null);
const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false);
const [sortDirection, setSortDirection] = useState<
"ASC" | "DESC" | undefined
>("ASC");
const [currentSortField, setCurrentSortField] = useState<string>("name");
const [iniLoad, setIniLoad] = useState<boolean>(false);
const [canShareFile, setCanShareFile] = useState<boolean>(false);
const [canPreviewFile, setCanPreviewFile] = useState<boolean>(false);
const [quota, setQuota] = useState<BucketQuota | null>(null);
const [downloadRenameModal, setDownloadRenameModal] =
useState<BucketObjectItem | null>(null);
const pathSegment = location.pathname.split("/browse/");
@@ -379,6 +308,30 @@ const ListObjects = () => {
true
);
const displayDeleteObject = hasPermission(bucketName, [
IAM_SCOPES.S3_DELETE_OBJECT,
]);
const selectedObjects = useSelector(
(state: AppState) => state.objectBrowser.selectedObjects
);
useEffect(() => {
dispatch(setSearchObjects(""));
dispatch(setLoadingObjects(true));
dispatch(setSelectedObjects([]));
}, [simplePath, dispatch]);
useEffect(() => {
if (rewindEnabled) {
if (bucketToRewind !== bucketName) {
dispatch(resetRewind());
return;
}
}
}, [rewindEnabled, bucketToRewind, bucketName, dispatch]);
// END OF WS HANDLERS
useEffect(() => {
if (folderUpload.current !== null) {
folderUpload.current.setAttribute("directory", "");
@@ -433,39 +386,14 @@ const ListObjects = () => {
return;
}
if (selectedObjects.length === 0 && selectedInternalPaths === null) {
if (
selectedObjects.length === 0 &&
selectedInternalPaths === null &&
!loadingObjects
) {
dispatch(setObjectDetailsView(false));
}
}, [selectedObjects, selectedInternalPaths, dispatch]);
const displayDeleteObject = hasPermission(bucketName, [
IAM_SCOPES.S3_DELETE_OBJECT,
]);
const displayListObjects = hasPermission(bucketName, [
IAM_SCOPES.S3_LIST_BUCKET,
]);
const updateMessage = () => {
let timeDelta = Date.now() - loadingStartTime;
if (timeDelta / 1000 >= 6) {
setLoadingMessage(
<Fragment>
<Typography component="h3">
This operation is taking longer than expected... (
{Math.ceil(timeDelta / 1000)}s)
</Typography>
</Fragment>
);
} else if (timeDelta / 1000 >= 3) {
setLoadingMessage(
<Typography component="h3">
This operation is taking longer than expected...
</Typography>
);
}
};
}, [selectedObjects, selectedInternalPaths, dispatch, loadingObjects]);
useEffect(() => {
if (!iniLoad) {
@@ -474,280 +402,6 @@ const ListObjects = () => {
}
}, [iniLoad, dispatch, setIniLoad]);
useInterval(() => {
// Your custom logic here
if (loading) {
updateMessage();
}
}, 1000);
useEffect(() => {
if (loadingVersioning) {
if (displayListObjects) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/versioning`)
.then((res: BucketVersioning) => {
setIsVersioned(res.is_versioned);
setLoadingVersioning(false);
})
.catch((err: ErrorResponseHandler) => {
console.error(
"Error Getting Object Versioning Status: ",
err.detailedError
);
setLoadingVersioning(false);
});
} else {
setLoadingVersioning(false);
setRecords([]);
}
}
}, [bucketName, loadingVersioning, dispatch, displayListObjects]);
useEffect(() => {
if (loadingLocking) {
if (displayListObjects) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/object-locking`)
.then((res: BucketObjectLocking) => {
setLockingEnabled(res.object_locking_enabled);
setLoadingLocking(false);
})
.catch((err: ErrorResponseHandler) => {
console.error(
"Error Getting Object Locking Status: ",
err.detailedError
);
setLoadingLocking(false);
});
} else {
setRecords([]);
setLoadingLocking(false);
}
}
}, [bucketName, loadingLocking, dispatch, displayListObjects]);
useEffect(() => {
const decodedIPaths = decodeURLString(internalPaths);
if (decodedIPaths.endsWith("/") || decodedIPaths === "") {
dispatch(setObjectDetailsView(false));
dispatch(setSelectedObjectView(null));
dispatch(
setSimplePathHandler(decodedIPaths === "" ? "/" : decodedIPaths)
);
} else {
dispatch(setLoadingObjectInfo(true));
dispatch(setObjectDetailsView(true));
dispatch(setLoadingVersions(true));
dispatch(
setSelectedObjectView(
`${decodedIPaths ? `${encodeURLString(decodedIPaths)}` : ``}`
)
);
dispatch(
setSimplePathHandler(
`${decodedIPaths.split("/").slice(0, -1).join("/")}/`
)
);
}
}, [internalPaths, rewindDate, rewindEnabled, dispatch]);
useEffect(() => {
dispatch(setSearchObjects(""));
dispatch(setLoadingObjectsList(true));
setSelectedObjects([]);
}, [simplePath, dispatch, setSelectedObjects]);
useEffect(() => {
if (loading) {
if (displayListObjects) {
let pathPrefix = "";
if (internalPaths) {
const decodedPath = decodeURLString(internalPaths);
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
let currentTimestamp = Date.now();
setLoadingStartTime(currentTimestamp);
setLoadingMessage(defLoading);
// We get URL to look into
let urlTake = `/api/v1/buckets/${bucketName}/objects`;
// Is rewind enabled?, we use Rewind API
if (rewindEnabled) {
if (bucketToRewind !== bucketName) {
dispatch(resetRewind());
return;
}
if (rewindDate) {
const rewindParsed = rewindDate.toISOString();
urlTake = `/api/v1/buckets/${bucketName}/rewind/${rewindParsed}`;
}
} else if (showDeleted) {
// Do we want to display deleted items too?, we use rewind to current time to show everything
const currDate = new Date();
const currDateISO = currDate.toISOString();
urlTake = `/api/v1/buckets/${bucketName}/rewind/${currDateISO}`;
}
api
.invoke(
"GET",
`${urlTake}${
pathPrefix ? `?prefix=${encodeURLString(pathPrefix)}` : ``
}`
)
.then((res: BucketObjectItemsList) => {
const records: BucketObjectItem[] = res.objects || [];
const folders: BucketObjectItem[] = [];
const files: BucketObjectItem[] = [];
// We separate items between folders or files to display folders at the beginning always.
records.forEach((record) => {
// We omit files from the same path
if (record.name !== decodeURLString(internalPaths)) {
// this is a folder
if (record.name.endsWith("/")) {
folders.push(record);
} else {
// this is a file
files.push(record);
}
}
});
const recordsInElement = [...folders, ...files];
if (recordsInElement.length === 0 && pathPrefix !== "") {
let pathTest = `/api/v1/buckets/${bucketName}/objects${
internalPaths ? `?prefix=${internalPaths}` : ""
}`;
if (rewindEnabled) {
const rewindParsed = rewindDate.toISOString();
let pathPrefix = "";
if (internalPaths) {
const decodedPath = decodeURLString(internalPaths);
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
pathTest = `/api/v1/buckets/${bucketName}/rewind/${rewindParsed}${
pathPrefix ? `?prefix=${encodeURLString(pathPrefix)}` : ``
}`;
}
api
.invoke("GET", pathTest)
.then((res: BucketObjectItemsList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (!res.objects) {
// It is a folder, we remove loader & set original results list
dispatch(setLoadingObjectsList(false));
setRecords(recordsInElement);
} else {
// This code prevents the program from opening a file when a substring of that file is entered as a new folder.
// Previously, if there was a file test1.txt and the folder test was created with the same prefix, the program
// would open test1.txt instead
let found = false;
let pathPrefixChopped = pathPrefix.slice(
0,
pathPrefix.length - 1
);
for (let i = 0; i < res.objects.length; i++) {
if (res.objects[i].name === pathPrefixChopped) {
found = true;
}
}
if (
(res.objects.length === 1 &&
res.objects[0].name.endsWith("/")) ||
!found
) {
// This is a folder, we set the original results list
setRecords(recordsInElement);
} else {
// This is a file. We change URL & Open file details view.
dispatch(setObjectDetailsView(true));
dispatch(setSelectedObjectView(internalPaths));
// We split the selected object URL & remove the last item to fetch the files list for the parent folder
const parentPath = `${decodeURLString(internalPaths)
.split("/")
.slice(0, -1)
.join("/")}/`;
api
.invoke(
"GET",
`${urlTake}${
pathPrefix
? `?prefix=${encodeURLString(parentPath)}`
: ``
}`
)
.then((res: BucketObjectItemsList) => {
const records: BucketObjectItem[] = res.objects || [];
setRecords(records);
})
.catch(() => {});
}
dispatch(setLoadingObjectsList(false));
}
})
.catch((err: ErrorResponseHandler) => {
dispatch(setLoadingObjectsList(false));
dispatch(setErrorSnackMessage(err));
});
} else {
setRecords(recordsInElement);
dispatch(setLoadingObjectsList(false));
}
})
.catch((err: ErrorResponseHandler) => {
const permitItems = permissionItems(
bucketName,
pathPrefix,
allowResources || []
);
if (!permitItems || permitItems.length === 0) {
dispatch(setErrorSnackMessage(err));
} else {
setRecords(permitItems);
}
dispatch(setLoadingObjectsList(false));
});
} else {
dispatch(setLoadingObjectsList(false));
}
}
}, [
loading,
dispatch,
bucketName,
rewindEnabled,
rewindDate,
internalPaths,
bucketInfo,
showDeleted,
displayListObjects,
bucketToRewind,
allowResources,
]);
// bucket info
useEffect(() => {
if (loadingBucket) {
@@ -769,8 +423,8 @@ const ListObjects = () => {
if (refresh) {
dispatch(setSnackBarMessage(`Objects deleted successfully.`));
setSelectedObjects([]);
dispatch(setLoadingObjectsList(true));
dispatch(setSelectedObjects([]));
dispatch(setLoadingObjects(true));
}
};
@@ -794,73 +448,6 @@ const ListObjects = () => {
e.target.value = "";
};
const downloadObject = (object: BucketObjectItem) => {
const identityDownload = encodeURLString(
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);
const ID = makeid(8);
const downloadCall = download(
bucketName,
encodeURLString(object.name),
object.version_id,
object.size,
null,
ID,
(progress) => {
dispatch(
updateProgress({
instanceID: identityDownload,
progress: progress,
})
);
},
() => {
dispatch(completeObject(identityDownload));
},
(msg: string) => {
dispatch(failObject({ instanceID: identityDownload, msg }));
},
() => {
dispatch(cancelObjectInList(identityDownload));
}
);
storeCallForObjectWithID(ID, downloadCall);
dispatch(
setNewObject({
ID,
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: object.name,
type: "download",
waitingForFile: true,
failed: false,
cancelled: false,
errorMessage: "",
})
);
};
const openPath = (idElement: string) => {
setSelectedObjects([]);
const newPath = `/buckets/${bucketName}/browse${
idElement ? `/${encodeURLString(idElement)}` : ``
}`;
navigate(newPath);
dispatch(setObjectDetailsView(true));
dispatch(setLoadingVersions(true));
dispatch(
setSelectedObjectView(
`${idElement ? `${encodeURLString(idElement)}` : ``}`
)
);
};
const uploadObject = useCallback(
(files: File[], folderPath: string): void => {
let pathPrefix = "";
@@ -1008,7 +595,7 @@ const ListObjects = () => {
};
xhr.onloadend = () => {
if (files.length === 0) {
dispatch(setLoadingObjectsList(true));
dispatch(setLoadingObjects(true));
}
};
xhr.onabort = () => {
@@ -1063,8 +650,8 @@ const ListObjects = () => {
dispatch(setErrorSnackMessage(err));
}
// We force objects list reload after all promises were handled
dispatch(setLoadingObjectsList(true));
setSelectedObjects([]);
dispatch(setLoadingObjects(true));
dispatch(setSelectedObjects([]));
});
};
@@ -1110,140 +697,18 @@ const ListObjects = () => {
[isDragActive, isDragAccept]
);
const openPreview = () => {
if (selectedObjects.length === 1) {
let fileObject: BucketObjectItem | undefined;
const findFunction = (currValue: BucketObjectItem) =>
selectedObjects.includes(currValue.name);
fileObject = filteredRecords.find(findFunction);
if (fileObject) {
setSelectedPreview(fileObject);
setPreviewOpen(true);
}
}
};
const openShare = () => {
if (selectedObjects.length === 1) {
let fileObject: BucketObjectItem | undefined;
const findFunction = (currValue: BucketObjectItem) =>
selectedObjects.includes(currValue.name);
fileObject = filteredRecords.find(findFunction);
if (fileObject) {
setSelectedPreview(fileObject);
setShareFileModalOpen(true);
}
}
};
const closeShareModal = () => {
setShareFileModalOpen(false);
setSelectedPreview(null);
dispatch(setShareFileModalOpen(false));
dispatch(setSelectedPreview(null));
};
const filteredRecords = records.filter((b: BucketObjectItem) => {
if (searchObjects === "") {
return true;
} else {
const objectName = b.name.toLowerCase();
if (objectName.indexOf(searchObjects.toLowerCase()) >= 0) {
return true;
} else {
return false;
}
}
});
const rewindCloseModal = () => {
setRewindSelect(false);
};
const closePreviewWindow = () => {
setPreviewOpen(false);
setSelectedPreview(null);
};
const selectListObjects = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: string[] = [...selectedObjects]; // We clone the selectedBuckets array
if (checked) {
// If the user has checked this field we need to push this to selectedBucketsList
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter((element) => element !== value);
}
setSelectedObjects(elements);
dispatch(setSelectedObjectView(null));
return elements;
};
const sortChange = (sortData: any) => {
const newSortDirection = get(sortData, "sortDirection", "DESC");
setCurrentSortField(sortData.sortBy);
setSortDirection(newSortDirection);
dispatch(setLoadingObjectsList(true));
};
const plSelect = filteredRecords;
const sortASC = plSelect.sort(sortListObjects(currentSortField));
let payload: BucketObjectItem[] = [];
if (sortDirection === "ASC") {
payload = sortASC;
} else {
payload = sortASC.reverse();
}
const selectAllItems = () => {
dispatch(setSelectedObjectView(null));
if (selectedObjects.length === payload.length) {
setSelectedObjects([]);
return;
}
const elements = payload.map((item) => item.name);
setSelectedObjects(elements);
};
const downloadSelected = () => {
if (selectedObjects.length !== 0) {
let itemsToDownload: BucketObjectItem[] = [];
const filterFunction = (currValue: BucketObjectItem) =>
selectedObjects.includes(currValue.name);
itemsToDownload = filteredRecords.filter(filterFunction);
// I case just one element is selected, then we trigger download modal validation.
// We are going to enforce zip download when multiple files are selected
if (itemsToDownload.length === 1) {
if (
itemsToDownload[0].name.length > 200 &&
getClientOS().toLowerCase().includes("win")
) {
setDownloadRenameModal(itemsToDownload[0]);
return;
}
}
itemsToDownload.forEach((filteredItem) => {
downloadObject(filteredItem);
});
}
dispatch(setPreviewOpen(false));
dispatch(setSelectedPreview(null));
};
const onClosePanel = (forceRefresh: boolean) => {
@@ -1268,36 +733,30 @@ const ListObjects = () => {
}
dispatch(setObjectDetailsView(false));
setSelectedObjects([]);
dispatch(setSelectedObjects([]));
if (forceRefresh) {
dispatch(setLoadingObjectsList(true));
dispatch(setLoadingObjects(true));
}
};
const setDeletedAction = () => {
dispatch(resetMessages());
dispatch(setShowDeletedObjects(!showDeleted));
onClosePanel(true);
};
const closeRenameModal = () => {
setDownloadRenameModal(null);
dispatch(setDownloadRenameModal(null));
};
const tableActions: ItemActions[] = [
{
type: "view",
label: "View",
onClick: openPath,
sendOnlyId: true,
},
];
const multiActionButtons = [
{
action: downloadSelected,
action: () => {
dispatch(downloadSelected(bucketName));
},
label: "Download",
disabled: !canDownload || selectedObjects.length === 0,
disabled: !canDownload || selectedObjects?.length === 0,
icon: <DownloadIcon />,
tooltip: canDownload
? "Download Selected"
@@ -1307,14 +766,18 @@ const ListObjects = () => {
),
},
{
action: openShare,
action: () => {
dispatch(openShare());
},
label: "Share",
disabled: selectedObjects.length !== 1 || !canShareFile,
icon: <ShareIcon />,
tooltip: canShareFile ? "Share Selected File" : "Sharing unavailable",
},
{
action: openPreview,
action: () => {
dispatch(openPreview());
},
label: "Preview",
disabled: selectedObjects.length !== 1 || !canPreviewFile,
icon: <PreviewIcon />,
@@ -1484,7 +947,9 @@ const ListObjects = () => {
if (versionsMode) {
dispatch(setLoadingVersions(true));
} else {
dispatch(setLoadingObjectsList(true));
dispatch(resetMessages());
dispatch(setLoadingRecords(true));
dispatch(setLoadingObjects(true));
}
}}
disabled={
@@ -1560,7 +1025,6 @@ const ListObjects = () => {
<BrowserBreadcrumbs
bucketName={bucketName}
internalPaths={pageTitle}
existingFiles={records || []}
additionalOptions={
!isVersioned || rewindEnabled ? null : (
<div>
@@ -1581,48 +1045,7 @@ const ListObjects = () => {
hidePathButton={false}
/>
</Grid>
<TableWrapper
itemActions={tableActions}
columns={
rewindEnabled ? rewindModeColumns : listModeColumns
}
isLoading={loading}
loadingMessage={loadingMessage}
entityName="Objects"
idField="name"
records={payload}
customPaperHeight={`${classes.browsePaper} ${
obOnly ? "isEmbedded" : ""
} ${detailsOpen ? "actionsPanelOpen" : ""}`}
selectedItems={selectedObjects}
onSelect={selectListObjects}
customEmptyMessage={
!displayListObjects
? permissionTooltipHelper(
[IAM_SCOPES.S3_LIST_BUCKET],
"view Objects in this bucket"
)
: `This location is empty${
!rewindEnabled
? ", please try uploading a new file"
: ""
}`
}
sortConfig={{
currentSort: currentSortField,
currentDirection: sortDirection,
triggerSort: sortChange,
}}
onSelectAll={selectAllItems}
rowStyle={({ index }) => {
if (payload[index]?.delete_flag) {
return "deleted";
}
return "";
}}
parentClassName={classes.parentWrapper}
/>
<ListObjectsTable />
</Grid>
</SecureComponent>
)}

View File

@@ -0,0 +1,246 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { listModeColumns, rewindModeColumns } from "./ListObjectsHelpers";
import TableWrapper, {
ItemActions,
} from "../../../../Common/TableWrapper/TableWrapper";
import React, { useState } from "react";
import makeStyles from "@mui/styles/makeStyles";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import { useSelector } from "react-redux";
import { AppState, useAppDispatch } from "../../../../../../store";
import { selFeatures } from "../../../../consoleSlice";
import { encodeURLString } from "../../../../../../common/utils";
import {
setLoadingObjects,
setLoadingVersions,
setObjectDetailsView,
setSelectedObjects,
setSelectedObjectView,
} from "../../../../ObjectBrowser/objectBrowserSlice";
import { useNavigate, useParams } from "react-router-dom";
import get from "lodash/get";
import { sortListObjects } from "../utils";
import { BucketObjectItem } from "./types";
import {
IAM_SCOPES,
permissionTooltipHelper,
} from "../../../../../../common/SecureComponent/permissions";
import { hasPermission } from "../../../../../../common/SecureComponent";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
browsePaper: {
border: 0,
height: "calc(100vh - 290px)",
"&.isEmbedded": {
height: "calc(100vh - 315px)",
},
"&.actionsPanelOpen": {
minHeight: "100%",
},
"@media (max-width: 800px)": {
width: 800,
},
},
parentWrapper: {
position: "relative",
height: "calc(100% - 60px)",
"@media (max-width: 800px)": {
overflowX: "auto",
},
},
"@global": {
".rowLine:hover .iconFileElm": {
backgroundImage: "url(/images/ob_file_filled.svg)",
},
".rowLine:hover .iconFolderElm": {
backgroundImage: "url(/images/ob_folder_filled.svg)",
},
},
})
);
const ListObjectsTable = () => {
const classes = useStyles();
const dispatch = useAppDispatch();
const params = useParams();
const navigate = useNavigate();
const [sortDirection, setSortDirection] = useState<
"ASC" | "DESC" | undefined
>("ASC");
const [currentSortField, setCurrentSortField] = useState<string>("name");
const bucketName = params.bucketName || "";
const detailsOpen = useSelector(
(state: AppState) => state.objectBrowser.objectDetailsOpen
);
const loadingObjects = useSelector(
(state: AppState) => state.objectBrowser.loadingObjects
);
const features = useSelector(selFeatures);
const obOnly = !!features?.includes("object-browser-only");
const rewindEnabled = useSelector(
(state: AppState) => state.objectBrowser.rewind.rewindEnabled
);
const records = useSelector((state: AppState) => state.objectBrowser.records);
const searchObjects = useSelector(
(state: AppState) => state.objectBrowser.searchObjects
);
const selectedObjects = useSelector(
(state: AppState) => state.objectBrowser.selectedObjects
);
const displayListObjects = hasPermission(bucketName, [
IAM_SCOPES.S3_LIST_BUCKET,
]);
const filteredRecords = records.filter((b: BucketObjectItem) => {
if (searchObjects === "") {
return true;
} else {
const objectName = b.name.toLowerCase();
if (objectName.indexOf(searchObjects.toLowerCase()) >= 0) {
return true;
} else {
return false;
}
}
});
const plSelect = filteredRecords;
const sortASC = plSelect.sort(sortListObjects(currentSortField));
let payload: BucketObjectItem[] = [];
if (sortDirection === "ASC") {
payload = sortASC;
} else {
payload = sortASC.reverse();
}
const openPath = (idElement: string) => {
dispatch(setSelectedObjects([]));
const newPath = `/buckets/${bucketName}/browse${
idElement ? `/${encodeURLString(idElement)}` : ``
}`;
navigate(newPath);
dispatch(setObjectDetailsView(true));
dispatch(setLoadingVersions(true));
dispatch(
setSelectedObjectView(
`${idElement ? `${encodeURLString(idElement)}` : ``}`
)
);
};
const tableActions: ItemActions[] = [
{
type: "view",
label: "View",
onClick: openPath,
sendOnlyId: true,
},
];
const sortChange = (sortData: any) => {
const newSortDirection = get(sortData, "sortDirection", "DESC");
setCurrentSortField(sortData.sortBy);
setSortDirection(newSortDirection);
dispatch(setLoadingObjects(true));
};
const selectAllItems = () => {
dispatch(setSelectedObjectView(null));
if (selectedObjects.length === payload.length) {
dispatch(setSelectedObjects([]));
return;
}
const elements = payload.map((item) => item.name);
dispatch(setSelectedObjects(elements));
};
const selectListObjects = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: string[] = [...selectedObjects]; // We clone the selectedBuckets array
if (checked) {
// If the user has checked this field we need to push this to selectedBucketsList
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter((element) => element !== value);
}
dispatch(setSelectedObjects(elements));
dispatch(setSelectedObjectView(null));
return elements;
};
return (
<TableWrapper
itemActions={tableActions}
columns={rewindEnabled ? rewindModeColumns : listModeColumns}
isLoading={loadingObjects}
entityName="Objects"
idField="name"
records={payload}
customPaperHeight={`${classes.browsePaper} ${
obOnly ? "isEmbedded" : ""
} ${detailsOpen ? "actionsPanelOpen" : ""}`}
selectedItems={selectedObjects}
onSelect={selectListObjects}
customEmptyMessage={
!displayListObjects
? permissionTooltipHelper(
[IAM_SCOPES.S3_LIST_BUCKET],
"view Objects in this bucket"
)
: `This location is empty${
!rewindEnabled ? ", please try uploading a new file" : ""
}`
}
sortConfig={{
currentSort: currentSortField,
currentDirection: sortDirection,
triggerSort: sortChange,
}}
onSelectAll={selectAllItems}
rowStyle={({ index }) => {
if (payload[index]?.delete_flag) {
return "deleted";
}
return "";
}}
parentClassName={classes.parentWrapper}
/>
);
};
export default ListObjectsTable;

View File

@@ -29,7 +29,7 @@ import {
spacingUtils,
textStyleUtils,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { IFileInfo } from "../ObjectDetails/types";
import { IFileInfo, MetadataResponse } from "../ObjectDetails/types";
import { download, extensionPreview } from "../utils";
import { ErrorResponseHandler } from "../../../../../../common/types";
@@ -189,6 +189,8 @@ const ObjectDetailPanel = ({
const [previewOpen, setPreviewOpen] = useState<boolean>(false);
const [totalVersionsSize, setTotalVersionsSize] = useState<number>(0);
const [longFileOpen, setLongFileOpen] = useState<boolean>(false);
const [metaData, setMetaData] = useState<any | null>(null);
const [loadMetadata, setLoadingMetadata] = useState<boolean>(false);
const internalPathsDecoded = decodeURLString(internalPaths) || "";
const allPathData = internalPathsDecoded.split("/");
@@ -212,6 +214,10 @@ const ObjectDetailPanel = ({
) || emptyFile;
}
if (!infoElement.is_delete_marker) {
setLoadingMetadata(true);
}
setActualInfo(infoElement);
}
}, [selectedVersion, distributedSetup, allInfoElements]);
@@ -242,8 +248,14 @@ const ObjectDetailPanel = ({
setTotalVersionsSize(tVersionSize);
} else {
setActualInfo(result[0]);
const resInfo = result[0];
setActualInfo(resInfo);
setVersions([]);
if (!resInfo.is_delete_marker) {
setLoadingMetadata(true);
}
}
dispatch(setLoadingObjectInfo(false));
@@ -262,6 +274,26 @@ const ObjectDetailPanel = ({
selectedVersion,
]);
useEffect(() => {
if (loadMetadata && internalPaths !== "") {
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects/metadata?prefix=${internalPaths}`
)
.then((res: MetadataResponse) => {
let metadata = get(res, "objectMetadata", {});
setMetaData(metadata);
setLoadingMetadata(false);
})
.catch((err) => {
console.error("Error Getting Metadata Status: ", err.detailedError);
setLoadingMetadata(false);
});
}
}, [bucketName, internalPaths, loadMetadata]);
let tagKeys: string[] = [];
if (actualInfo && actualInfo.tags) {
@@ -649,7 +681,7 @@ const ObjectDetailPanel = ({
version_id: actualInfo.version_id || "null",
size: parseInt(actualInfo.size || "0"),
content_type: "",
last_modified: new Date(actualInfo.last_modified),
last_modified: actualInfo.last_modified,
}}
onClosePreview={() => {
setPreviewOpen(false);
@@ -838,20 +870,19 @@ const ObjectDetailPanel = ({
</Fragment>
</SecureComponent>
</Box>
<Grid item xs={12} className={classes.headerForSection}>
<span>Metadata</span>
<MetadataIcon />
</Grid>
<Box className={classes.detailContainer}>
{actualInfo ? (
<ObjectMetaData
bucketName={bucketName}
internalPaths={internalPaths}
actualInfo={actualInfo}
linear
/>
) : null}
</Box>
{!actualInfo.is_delete_marker && (
<Fragment>
<Grid item xs={12} className={classes.headerForSection}>
<span>Metadata</span>
<MetadataIcon />
</Grid>
<Box className={classes.detailContainer}>
{actualInfo && metaData ? (
<ObjectMetaData metaData={metaData} linear />
) : null}
</Box>
</Fragment>
)}
</Fragment>
)}
</Fragment>

View File

@@ -25,7 +25,7 @@ import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapp
import { AppState, useAppDispatch } from "../../../../../../store";
import {
resetRewind,
setLoadingObjectsList,
setLoadingObjects,
setRewindEnable,
} from "../../../../ObjectBrowser/objectBrowserSlice";
@@ -73,7 +73,7 @@ const RewindEnable = ({
})
);
}
dispatch(setLoadingObjectsList(true));
dispatch(setLoadingObjects(true));
closeModalAndRefresh();
};

View File

@@ -14,17 +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 { IFileInfo } from "../ObjectDetails/types";
export interface BucketObjectItem {
name: string;
size: number;
etag?: string;
last_modified: Date;
last_modified: string;
content_type?: string;
version_id: string;
delete_flag?: boolean;
is_latest?: boolean;
}
export interface BucketObjectItemsList {
objects: BucketObjectItem[];
total?: number;
}
export interface WebsocketRequest {
mode: "objects" | "rewind" | "close" | "cancel";
bucket_name?: string;
prefix?: string;
date?: string;
request_id: number;
}
export interface WebsocketResponse {
request_id: number;
error?: string;
request_end?: boolean;
data?: ObjectResponse[];
}
export interface ObjectResponse {
name: string;
last_modified: string;
size: number;
version_id: string;
delete_flag: boolean;
is_latest: boolean;
}
export interface IRestoreLocalObjectList {
prefix: string;
objectInfo: IFileInfo;
}

View File

@@ -1,8 +1,21 @@
import React, { Fragment, useCallback, useEffect, useState } from "react";
import useApi from "../../../../Common/Hooks/useApi";
import { ErrorResponseHandler } from "../../../../../../common/types";
import { MetadataResponse } from "./types";
import get from "lodash/get";
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment } from "react";
import { withStyles } from "@mui/styles";
import Grid from "@mui/material/Grid";
import { Box, Table, TableBody, TableCell, TableRow } from "@mui/material";
import { Theme } from "@mui/material/styles";
@@ -11,13 +24,10 @@ import {
detailsPanel,
spacingUtils,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { withStyles } from "@mui/styles";
interface IObjectMetadata {
bucketName: string;
internalPaths: string;
metaData: any;
classes?: any;
actualInfo: any;
linear?: boolean;
}
@@ -45,38 +55,11 @@ const styles = (theme: Theme) =>
});
const ObjectMetaData = ({
bucketName,
internalPaths,
metaData,
classes,
actualInfo,
linear = false,
}: IObjectMetadata) => {
const [metaData, setMetaData] = useState<any>({});
const onMetaDataSuccess = (res: MetadataResponse) => {
let metadata = get(res, "objectMetadata", {});
setMetaData(metadata);
};
const onMetaDataError = (err: ErrorResponseHandler) => false;
const [, invokeMetaDataApi] = useApi(onMetaDataSuccess, onMetaDataError);
const metaKeys = Object.keys(metaData);
const loadMetaData = useCallback(() => {
invokeMetaDataApi(
"GET",
`/api/v1/buckets/${bucketName}/objects/metadata?prefix=${internalPaths}`
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bucketName, internalPaths, actualInfo]);
useEffect(() => {
if (actualInfo) {
loadMetaData();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actualInfo, loadMetaData]);
if (linear) {
return (

View File

@@ -29,12 +29,14 @@ import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog";
import RecoverIcon from "../../../../../../icons/RecoverIcon";
import { setErrorSnackMessage } from "../../../../../../systemSlice";
import { useAppDispatch } from "../../../../../../store";
import { IFileInfo } from "./types";
import { restoreLocalObjectList } from "../../../../ObjectBrowser/objectBrowserSlice";
interface IRestoreFileVersion {
classes: any;
restoreOpen: boolean;
bucketName: string;
versionID: string;
versionToRestore: IFileInfo;
objectPath: string;
onCloseAndUpdate: (refresh: boolean) => void;
}
@@ -46,7 +48,7 @@ const styles = (theme: Theme) =>
const RestoreFileVersion = ({
classes,
versionID,
versionToRestore,
bucketName,
objectPath,
restoreOpen,
@@ -63,11 +65,18 @@ const RestoreFileVersion = ({
"PUT",
`/api/v1/buckets/${bucketName}/objects/restore?prefix=${encodeURLString(
objectPath
)}&version_id=${versionID}`
)}&version_id=${versionToRestore.version_id}`
)
.then((res: any) => {
console.log("REStORE", res);
setRestoreLoading(false);
onCloseAndUpdate(true);
dispatch(
restoreLocalObjectList({
prefix: objectPath,
objectInfo: versionToRestore,
})
);
})
.catch((error: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(error));
@@ -95,7 +104,7 @@ const RestoreFileVersion = ({
Are you sure you want to restore <br />
<b>{objectPath}</b> <br /> with Version ID:
<br />
<b className={classes.wrapText}>{versionID}</b>?
<b className={classes.wrapText}>{versionToRestore.version_id}</b>?
</DialogContentText>
}
/>

View File

@@ -181,7 +181,7 @@ const VersionsNavigator = ({
const [objectToShare, setObjectToShare] = useState<IFileInfo | null>(null);
const [versions, setVersions] = useState<IFileInfo[]>([]);
const [restoreVersionOpen, setRestoreVersionOpen] = useState<boolean>(false);
const [restoreVersion, setRestoreVersion] = useState<string>("");
const [restoreVersion, setRestoreVersion] = useState<IFileInfo | null>(null);
const [sortValue, setSortValue] = useState<string>("date");
const [previewOpen, setPreviewOpen] = useState<boolean>(false);
const [deleteNonCurrentOpen, setDeleteNonCurrentOpen] =
@@ -313,7 +313,7 @@ const VersionsNavigator = ({
};
const onRestoreItem = (item: IFileInfo) => {
setRestoreVersion(item.version_id || "");
setRestoreVersion(item);
setRestoreVersionOpen(true);
};
@@ -334,7 +334,7 @@ const VersionsNavigator = ({
const closeRestoreModal = (reloadObjectData: boolean) => {
setRestoreVersionOpen(false);
setRestoreVersion("");
setRestoreVersion(null);
if (reloadObjectData) {
dispatch(setLoadingVersions(true));
@@ -454,11 +454,11 @@ const VersionsNavigator = ({
dataObject={objectToShare || actualInfo}
/>
)}
{restoreVersionOpen && actualInfo && (
{restoreVersionOpen && actualInfo && restoreVersion && (
<RestoreFileVersion
restoreOpen={restoreVersionOpen}
bucketName={bucketName}
versionID={restoreVersion}
versionToRestore={restoreVersion}
objectPath={actualInfo.name}
onCloseAndUpdate={closeRestoreModal}
/>
@@ -477,7 +477,7 @@ const VersionsNavigator = ({
objectToShare && objectToShare.size ? objectToShare.size : "0"
),
content_type: "",
last_modified: new Date(actualInfo.last_modified),
last_modified: actualInfo.last_modified,
}}
onClosePreview={() => {
setPreviewOpen(false);
@@ -514,7 +514,6 @@ const VersionsNavigator = ({
<BrowserBreadcrumbs
bucketName={bucketName}
internalPaths={decodeURLString(internalPaths)}
existingFiles={[]}
hidePathButton={true}
/>
</Grid>

View File

@@ -247,7 +247,7 @@ export const permissionItems = (
returnElements.push({
name: `${currentElementInPath}/`,
size: 0,
last_modified: new Date(),
last_modified: "",
version_id: "",
});
}
@@ -276,7 +276,7 @@ export const permissionItems = (
pathToRouteElements.length > 0 ? "/" : ""
}${splitElement}/`,
size: 0,
last_modified: new Date(),
last_modified: "",
version_id: "",
});
return false;

View File

@@ -122,6 +122,24 @@ const AccountCreate = React.lazy(
const Users = React.lazy(() => import("./Users/Users"));
const Groups = React.lazy(() => import("./Groups/Groups"));
const IDPLDAPConfigurations = React.lazy(
() => import("./IDP/IDPLDAPConfigurations")
);
const IDPOpenIDConfigurations = React.lazy(
() => import("./IDP/IDPOpenIDConfigurations")
);
const AddIDPLDAPConfiguration = React.lazy(
() => import("./IDP/AddIDPLDAPConfiguration")
);
const AddIDPOpenIDConfiguration = React.lazy(
() => import("./IDP/AddIDPOpenIDConfiguration")
);
const IDPLDAPConfigurationDetails = React.lazy(
() => import("./IDP/IDPLDAPConfigurationDetails")
);
const IDPOpenIDConfigurationDetails = React.lazy(
() => import("./IDP/IDPOpenIDConfigurationDetails")
);
const TenantDetails = React.lazy(
() => import("./Tenants/TenantDetails/TenantDetails")
@@ -343,6 +361,30 @@ const Console = ({ classes }: IConsoleProps) => {
component: Policies,
path: IAM_PAGES.POLICIES,
},
{
component: IDPLDAPConfigurations,
path: IAM_PAGES.IDP_LDAP_CONFIGURATIONS,
},
{
component: IDPOpenIDConfigurations,
path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS,
},
{
component: AddIDPLDAPConfiguration,
path: IAM_PAGES.IDP_LDAP_CONFIGURATIONS_ADD,
},
{
component: AddIDPOpenIDConfiguration,
path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS_ADD,
},
{
component: IDPLDAPConfigurationDetails,
path: IAM_PAGES.IDP_LDAP_CONFIGURATIONS_VIEW,
},
{
component: IDPOpenIDConfigurationDetails,
path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS_VIEW,
},
{
component: Heal,
path: IAM_PAGES.TOOLS_HEAL,

View File

@@ -0,0 +1,219 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Box, Grid } from "@mui/material";
import {
formFieldStyles,
modalBasic,
} from "../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { Button } from "mds";
import { useNavigate } from "react-router-dom";
import { ErrorResponseHandler } from "../../../common/types";
import { useAppDispatch } from "../../../store";
import {
setErrorSnackMessage,
setServerNeedsRestart,
} from "../../../systemSlice";
import useApi from "../Common/Hooks/useApi";
import PageHeader from "../Common/PageHeader/PageHeader";
import BackLink from "../../../common/BackLink";
import PageLayout from "../Common/Layout/PageLayout";
import SectionTitle from "../Common/SectionTitle";
type AddIDPConfigurationProps = {
classes?: any;
icon: React.ReactNode;
helpBox: React.ReactNode;
header: string;
title: string;
backLink: string;
formFields: object;
endpoint: string;
};
const styles = (theme: Theme) =>
createStyles({
...formFieldStyles,
...modalBasic,
});
const AddIDPConfiguration = ({
classes,
icon,
helpBox,
header,
backLink,
title,
formFields,
endpoint,
}: AddIDPConfigurationProps) => {
const extraFormFields = {
name: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Config Name is required" : "";
},
label: "Name",
tooltip: "Name for identity provider configuration",
placeholder: "Name",
type: "text",
},
...formFields,
};
const navigate = useNavigate();
const dispatch = useAppDispatch();
const [fields, setFields] = useState<any>({});
const onSuccess = (res: any) => {
navigate(backLink);
dispatch(setServerNeedsRestart(res.restart === true));
};
const onError = (err: ErrorResponseHandler) =>
dispatch(setErrorSnackMessage(err));
const [loading, invokeApi] = useApi(onSuccess, onError);
const validSave = () => {
for (const [key, value] of Object.entries(extraFormFields)) {
if (
value.required &&
!(
fields[key] !== undefined &&
fields[key] !== null &&
fields[key] !== ""
)
) {
return false;
}
}
return true;
};
const resetForm = () => {
setFields({});
};
const addRecord = (event: React.FormEvent) => {
event.preventDefault();
const name = fields["name"];
let input = "";
for (const key of Object.keys(formFields)) {
if (fields[key]) {
input += `${key}=${fields[key]} `;
}
}
invokeApi("POST", endpoint, { name, input });
};
return (
<Grid item xs={12}>
<PageHeader label={<BackLink to={backLink} label={header} />} />
<PageLayout>
<Box
sx={{
display: "grid",
padding: "25px",
gap: "25px",
gridTemplateColumns: {
md: "2fr 1.2fr",
xs: "1fr",
},
border: "1px solid #eaeaea",
}}
>
<Box>
<SectionTitle icon={icon}>{title}</SectionTitle>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
addRecord(e);
}}
>
<Grid container item spacing="20" sx={{ marginTop: 1 }}>
<Grid xs={12} item>
{Object.entries(extraFormFields).map(([key, value]) => (
<Grid
item
xs={12}
className={classes.formFieldRow}
key={key}
>
<InputBoxWrapper
id={key}
required={value.required}
name={key}
label={value.label}
tooltip={value.tooltip}
error={value.hasError(fields[key], true)}
value={fields[key] ? fields[key] : ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFields({ ...fields, [key]: e.target.value })
}
placeholder={value.placeholder}
type={value.type}
/>
</Grid>
))}
<Grid item xs={12} textAlign={"right"}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginTop: "20px",
gap: "15px",
}}
>
<Button
id={"clear"}
type="button"
variant="regular"
onClick={resetForm}
label={"Clear"}
/>
<Button
id={"save-key"}
type="submit"
variant="callAction"
color="primary"
disabled={loading || !validSave()}
label={"Save"}
/>
</Box>
</Grid>
</Grid>
</Grid>
</form>
</Box>
{helpBox}
</Box>
</PageLayout>
</Grid>
);
};
export default withStyles(styles)(AddIDPConfiguration);

View File

@@ -0,0 +1,106 @@
import React, { Fragment } from "react";
import { Box } from "@mui/material";
import { HelpIconFilled } from "../../../icons";
interface IContent {
icon: React.ReactNode;
text: string;
iconDescription: string;
}
interface IAddIDPConfigurationHelpBoxProps {
helpText: string;
docLink: string;
docText: string;
contents: IContent[];
}
const FeatureItem = ({
icon,
description,
}: {
icon: any;
description: string;
}) => {
return (
<Box
sx={{
display: "flex",
"& .min-icon": {
marginRight: "10px",
height: "23px",
width: "23px",
marginBottom: "10px",
},
}}
>
{icon}{" "}
<div style={{ fontSize: "14px", fontStyle: "italic", color: "#5E5E5E" }}>
{description}
</div>
</Box>
);
};
const AddIDPConfigurationHelpBox = ({
helpText,
docLink,
docText,
contents,
}: IAddIDPConfigurationHelpBoxProps) => {
return (
<Box
sx={{
flex: 1,
border: "1px solid #eaeaea",
borderRadius: "2px",
display: "flex",
flexFlow: "column",
padding: "20px",
}}
>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
display: "flex",
alignItems: "center",
marginBottom: "16px",
paddingBottom: "20px",
"& .min-icon": {
height: "21px",
width: "21px",
marginRight: "15px",
},
}}
>
<HelpIconFilled />
<div>{helpText}</div>
</Box>
<Box sx={{ fontSize: "14px", marginBottom: "15px" }}>
{contents.map((content) => (
<Fragment>
{content.icon && (
<Box sx={{ paddingBottom: "20px" }}>
<FeatureItem
icon={content.icon}
description={content.iconDescription}
/>
</Box>
)}
<Box sx={{ paddingBottom: "20px" }}>{content.text}</Box>
</Fragment>
))}
<Box sx={{ paddingBottom: "20px" }}>
<a href={docLink} target="_blank" rel="noreferrer">
{docText}
</a>
</Box>
</Box>
</Box>
);
};
export default AddIDPConfigurationHelpBox;

View File

@@ -0,0 +1,86 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import LoginIcon from "@mui/icons-material/Login";
import {
formFieldStyles,
modalBasic,
} from "../Common/FormComponents/common/styleLibrary";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import AddIDPConfiguration from "./AddIDPConfiguration";
import { ldapFormFields } from "./utils";
import AddIDPConfigurationHelpBox from "./AddIDPConfigurationHelpbox";
type AddIDPLDAPConfigurationProps = {
classes?: any;
};
const styles = (theme: Theme) =>
createStyles({
...formFieldStyles,
formFieldRow: {
...formFieldStyles.formFieldRow,
},
...modalBasic,
});
const AddIDPLDAPConfiguration = ({ classes }: AddIDPLDAPConfigurationProps) => {
const helpBoxContents = [
{
text: "MinIO supports using an Active Directory or LDAP (AD/LDAP) service for external management of user identities. Configuring an external IDentity Provider (IDP) enables Single-Sign On (SSO) workflows, where applications authenticate against the external IDP before accessing MinIO.",
icon: <LoginIcon />,
iconDescription: "Create Configurations",
},
{
text: "MinIO queries the configured Active Directory / LDAP server to verify the credentials specified by the application and optionally return a list of groups in which the user has membership. MinIO supports two modes (Lookup-Bind Mode and Username-Bind Mode) for performing these queries",
icon: null,
iconDescription: "",
},
{
text: "MinIO recommends using Lookup-Bind mode as the preferred method for verifying AD/LDAP credentials. Username-Bind mode is a legacy method retained for backwards compatibility only.",
icon: null,
iconDescription: "",
},
];
return (
<AddIDPConfiguration
icon={<LoginIcon />}
helpBox={
<AddIDPConfigurationHelpBox
helpText={"Learn more about LDAP Configurations"}
contents={helpBoxContents}
docLink={
"https://min.io/docs/minio/linux/operations/external-iam.html?ref=con#minio-external-iam-ad-ldap"
}
docText={"Learn more about LDAP Configurations"}
/>
}
header={"LDAP Configurations"}
backLink={IAM_PAGES.IDP_LDAP_CONFIGURATIONS}
title={"Create LDAP Configuration"}
endpoint={"/api/v1/idp/ldap/"}
formFields={ldapFormFields}
/>
);
};
export default withStyles(styles)(AddIDPLDAPConfiguration);

View File

@@ -0,0 +1,71 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import { LockIcon } from "../../../icons";
import AddIDPConfiguration from "./AddIDPConfiguration";
import { openIDFormFields } from "./utils";
import AddIDPConfigurationHelpBox from "./AddIDPConfigurationHelpbox";
type AddIDPOpenIDConfigurationProps = {
classes?: any;
};
const styles = (theme: Theme) => createStyles({});
const AddIDPOpenIDConfiguration = ({
classes,
}: AddIDPOpenIDConfigurationProps) => {
const helpBoxContents = [
{
text: "MinIO supports using an OpenID Connect (OIDC) compatible IDentity Provider (IDP) such as Okta, KeyCloak, Dex, Google, or Facebook for external management of user identities.",
icon: <LockIcon />,
iconDescription: "Create Configurations",
},
{
text: "Configuring an external IDP enables Single-Sign On workflows, where applications authenticate against the external IDP before accessing MinIO.",
icon: null,
iconDescription: "",
},
];
return (
<AddIDPConfiguration
icon={<LockIcon />}
helpBox={
<AddIDPConfigurationHelpBox
helpText={"Learn more about OpenID Connect Configurations"}
contents={helpBoxContents}
docLink={
"https://min.io/docs/minio/linux/operations/external-iam.html?ref=con#minio-external-iam-oidc"
}
docText={"Learn more about OpenID Connect Configurations"}
/>
}
header={"OpenID Configurations"}
backLink={IAM_PAGES.IDP_OPENID_CONFIGURATIONS}
title={"Create OpenID Configuration"}
endpoint={"/api/v1/idp/openid/"}
formFields={openIDFormFields}
/>
);
};
export default withStyles(styles)(AddIDPOpenIDConfiguration);

View File

@@ -0,0 +1,86 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { DialogContentText } from "@mui/material";
import { ErrorResponseHandler } from "../../../common/types";
import useApi from "../Common/Hooks/useApi";
import ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog";
import { ConfirmDeleteIcon } from "../../../icons";
import {
setErrorSnackMessage,
setServerNeedsRestart,
} from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
interface IDeleteIDPConfigurationModalProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
idp: string;
idpType: string;
}
const DeleteIDPConfigurationModal = ({
closeDeleteModalAndRefresh,
deleteOpen,
idp,
idpType,
}: IDeleteIDPConfigurationModalProps) => {
const dispatch = useAppDispatch();
const onDelSuccess = (res: any) => {
closeDeleteModalAndRefresh(true);
dispatch(setServerNeedsRestart(res.restart === true));
};
const onDelError = (err: ErrorResponseHandler) =>
dispatch(setErrorSnackMessage(err));
const onClose = () => closeDeleteModalAndRefresh(false);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!idp) {
return null;
}
const onConfirmDelete = () => {
invokeDeleteApi("DELETE", `/api/v1/idp/${idpType}/${idp}`);
};
const displayName = idp === "_" ? "Default" : idp;
return (
<ConfirmDialog
title={`Delete ${displayName}`}
confirmText={"Delete"}
isOpen={deleteOpen}
titleIcon={<ConfirmDeleteIcon />}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmButtonProps={{
disabled: deleteLoading,
}}
confirmationContent={
<DialogContentText>
Are you sure you want to delete IDP <b>{displayName}</b>{" "}
configuration? <br />
</DialogContentText>
}
/>
);
};
export default DeleteIDPConfigurationModal;

View File

@@ -0,0 +1,364 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Box, Grid } from "@mui/material";
import {
buttonsStyles,
containerForHeader,
formFieldStyles,
hrClass,
modalBasic,
pageContentStyles,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import { RefreshIcon, TrashIcon } from "../../../icons";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { Button } from "mds";
import { useNavigate, useParams } from "react-router-dom";
import { ErrorResponseHandler } from "../../../common/types";
import { useAppDispatch } from "../../../store";
import {
setErrorSnackMessage,
setServerNeedsRestart,
} from "../../../systemSlice";
import useApi from "../Common/Hooks/useApi";
import api from "../../../common/api";
import PageLayout from "../Common/Layout/PageLayout";
import PageHeader from "../Common/PageHeader/PageHeader";
import BackLink from "../../../common/BackLink";
import ScreenTitle from "../Common/ScreenTitle/ScreenTitle";
import DeleteIDPConfigurationModal from "./DeleteIDPConfigurationModal";
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
type IDPConfigurationDetailsProps = {
classes?: any;
formFields: object;
endpoint: string;
backLink: string;
header: string;
idpType: string;
icon: React.ReactNode;
};
const styles = (theme: Theme) =>
createStyles({
...formFieldStyles,
formFieldRow: {
...formFieldStyles.formFieldRow,
},
...modalBasic,
pageContainer: {
height: "100%",
},
screenTitle: {
border: 0,
paddingTop: 0,
},
...pageContentStyles,
...searchField,
capitalize: {
textTransform: "capitalize",
},
...hrClass,
...buttonsStyles,
...containerForHeader(theme.spacing(4)),
});
const IDPConfigurationDetails = ({
classes,
formFields,
endpoint,
backLink,
header,
idpType,
icon,
}: IDPConfigurationDetailsProps) => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const params = useParams();
const configurationName = params.idpName;
const [loading, setLoading] = useState<boolean>(true);
const [isEnabled, setIsEnabled] = useState<boolean>(false);
const [fields, setFields] = useState<any>({});
const [originalFields, setOriginalFields] = useState<any>({});
const [record, setRecord] = useState<any>({});
const [editMode, setEditMode] = useState<boolean>(false);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const onSuccess = (res: any) => {
dispatch(setServerNeedsRestart(res.restart === true));
};
const onError = (err: ErrorResponseHandler) =>
dispatch(setErrorSnackMessage(err));
const [loadingSave, invokeApi] = useApi(onSuccess, onError);
const onEnabledSuccess = (res: any) => {
setIsEnabled(!isEnabled);
dispatch(setServerNeedsRestart(res.restart === true));
};
const onEnabledError = (err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
};
const [loadingEnabledSave, invokeEnabledApi] = useApi(
onEnabledSuccess,
onEnabledError
);
const toggleEditMode = () => {
if (editMode) {
parseFields(record);
}
setEditMode(!editMode);
};
const parseFields = (record: any) => {
let fields: any = {};
if (record.info) {
record.info.forEach((item: any) => {
if (item.key === "enable") {
setIsEnabled(item.value === "on");
}
fields[item.key] = item.value;
});
}
setFields(fields);
};
const parseOriginalFields = (record: any) => {
let fields: any = {};
if (record.info) {
record.info.forEach((item: any) => {
fields[item.key] = item.value;
});
}
setOriginalFields(fields);
};
useEffect(() => {
setLoading(true);
}, []);
useEffect(() => {
const loadRecord = () => {
api
.invoke("GET", `${endpoint}${configurationName}`)
.then((result: any) => {
if (result) {
setRecord(result);
parseFields(result);
parseOriginalFields(result);
}
setLoading(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoading(false);
});
};
if (loading) {
loadRecord();
}
}, [dispatch, loading, configurationName, endpoint]);
const validSave = () => {
for (const [key, value] of Object.entries(formFields)) {
if (
value.required &&
!(
fields[key] !== undefined &&
fields[key] !== null &&
fields[key] !== ""
)
) {
return false;
}
}
return true;
};
const resetForm = () => {
setFields({});
};
const saveRecord = (event: React.FormEvent) => {
event.preventDefault();
let input = "";
for (const key of Object.keys(formFields)) {
if (fields[key] || fields[key] !== originalFields[key]) {
input += `${key}=${fields[key]} `;
}
}
invokeApi("PUT", `${endpoint}${configurationName}`, { input });
setEditMode(false);
};
const closeDeleteModalAndRefresh = async (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
navigate(backLink);
}
};
const toggleConfiguration = (value: boolean) => {
const input = `enable=${value ? "on" : "off"}`;
invokeEnabledApi("PUT", `${endpoint}${configurationName}`, { input });
};
return (
<Grid item xs={12}>
{deleteOpen && configurationName && (
<DeleteIDPConfigurationModal
deleteOpen={deleteOpen}
idp={configurationName}
idpType={idpType}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<PageHeader
label={<BackLink to={backLink} label={header} />}
actions={
<FormSwitchWrapper
label={""}
indicatorLabels={["Enabled", "Disabled"]}
checked={isEnabled}
value={"is-configuration-enabled"}
id={"is-configuration-enabled"}
name={"is-configuration-enabled"}
onChange={(e) => toggleConfiguration(e.target.checked)}
description=""
disabled={loadingEnabledSave}
/>
}
/>
<PageLayout className={classes.pageContainer}>
<Grid item xs={12}>
<ScreenTitle
classes={{
screenTitle: classes.screenTitle,
}}
icon={icon}
title={configurationName === "_" ? "Default" : configurationName}
actions={
<Fragment>
{configurationName !== "_" && (
<Button
id={"delete-idp-config"}
onClick={() => {
setDeleteOpen(true);
}}
label={"Delete Configuration"}
icon={<TrashIcon />}
variant={"secondary"}
/>
)}
<Button
id={"refresh-idp-config"}
onClick={() => setLoading(true)}
label={"Refresh"}
icon={<RefreshIcon />}
/>
</Fragment>
}
/>
</Grid>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
saveRecord(e);
}}
>
<Grid container item spacing="20" sx={{ marginTop: 1 }}>
<Grid xs={12} item className={classes.fieldBox}>
{Object.entries(formFields).map(([key, value]) => (
<Grid item xs={12} className={classes.formFieldRow} key={key}>
<InputBoxWrapper
id={key}
required={value.required}
name={key}
label={value.label}
tooltip={value.tooltip}
error={value.hasError(fields[key], editMode)}
value={fields[key] ? fields[key] : ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setFields({ ...fields, [key]: e.target.value })
}
placeholder={value.placeholder}
disabled={!editMode}
type={value.type}
/>
</Grid>
))}
<Grid item xs={12} textAlign={"right"}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginTop: "20px",
gap: "15px",
}}
>
<Button
id={"edit"}
type="button"
variant={editMode ? "regular" : "callAction"}
onClick={toggleEditMode}
label={editMode ? "Cancel" : "Edit"}
/>
{editMode && (
<Button
id={"clear"}
type="button"
variant="regular"
onClick={resetForm}
label={"Clear"}
/>
)}
{editMode && (
<Button
id={"save-key"}
type="submit"
variant="callAction"
color="primary"
disabled={loading || loadingSave || !validSave()}
label={"Save"}
/>
)}
</Box>
</Grid>
</Grid>
</Grid>
</form>
</PageLayout>
</Grid>
);
};
export default withStyles(styles)(IDPConfigurationDetails);

View File

@@ -0,0 +1,225 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { useAppDispatch } from "../../../store";
import { useNavigate } from "react-router-dom";
import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
} from "../../../common/SecureComponent/permissions";
import {
hasPermission,
SecureComponent,
} from "../../../common/SecureComponent";
import api from "../../../common/api";
import { ErrorResponseHandler } from "../../../common/types";
import { setErrorSnackMessage } from "../../../systemSlice";
import PageHeader from "../Common/PageHeader/PageHeader";
import PageLayout from "../Common/Layout/PageLayout";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import { Grid } from "@mui/material";
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
import { Button } from "mds";
import { AddIcon, RefreshIcon } from "../../../icons";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import DeleteIDPConfigurationModal from "./DeleteIDPConfigurationModal";
type IDPConfigurationsProps = {
classes?: any;
idpType: string;
};
const styles = (theme: Theme) =>
createStyles({
...containerForHeader(theme.spacing(4)),
});
const IDPConfigurations = ({ classes, idpType }: IDPConfigurationsProps) => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedIDP, setSelectedIDP] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [records, setRecords] = useState<[]>([]);
const deleteIDP = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
]);
const viewIDP = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
]);
const displayIDPs = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
]);
useEffect(() => {
fetchRecords();
}, []);
useEffect(() => {
if (loading) {
if (displayIDPs) {
api
.invoke("GET", `/api/v1/idp/${idpType}`)
.then((res) => {
setLoading(false);
setRecords(
res.results.map((r: any) => {
r.name = r.name === "_" ? "Default" : r.name;
r.enabled = r.enabled === true ? "Enabled" : "Disabled";
return r;
})
);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
dispatch(setErrorSnackMessage(err));
});
} else {
setLoading(false);
}
}
}, [loading, setLoading, setRecords, dispatch, displayIDPs, idpType]);
const fetchRecords = () => {
setLoading(true);
};
const confirmDeleteIDP = (idp: string) => {
setDeleteOpen(true);
idp = idp === "Default" ? "_" : idp;
setSelectedIDP(idp);
};
const viewAction = (idp: any) => {
let name = idp.name === "Default" ? "_" : idp.name;
navigate(`/idp/${idpType}/configurations/${name}`);
};
const closeDeleteModalAndRefresh = async (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
fetchRecords();
}
};
const tableActions = [
{
type: "view",
onClick: viewAction,
disableButtonFunction: () => !viewIDP,
},
{
type: "delete",
onClick: confirmDeleteIDP,
sendOnlyId: true,
disableButtonFunction: (idp: string) => !deleteIDP || idp === "Default",
},
];
return (
<Fragment>
{deleteOpen && (
<DeleteIDPConfigurationModal
deleteOpen={deleteOpen}
idp={selectedIDP}
idpType={idpType}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<PageHeader label={`${idpType.toUpperCase()} Configurations`} />
<PageLayout className={classes.pageContainer}>
<Grid container spacing={1}>
<Grid
item
xs={12}
display={"flex"}
alignItems={"center"}
justifyContent={"flex-end"}
sx={{
"& button": {
marginLeft: "8px",
},
}}
>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_CONFIG_UPDATE]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TooltipWrapper tooltip={"Refresh"}>
<Button
id={"refresh-keys"}
variant="regular"
icon={<RefreshIcon />}
onClick={() => setLoading(true)}
/>
</TooltipWrapper>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_CONFIG_UPDATE]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TooltipWrapper tooltip={`Create ${idpType} configuration`}>
<Button
id={"create-idp"}
label={"Create Configuration"}
variant={"callAction"}
icon={<AddIcon />}
onClick={() =>
navigate(`/idp/${idpType}/configurations/add-idp`)
}
/>
</TooltipWrapper>
</SecureComponent>
</Grid>
<Grid item xs={12} className={classes.tableBlock}>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_CONFIG_UPDATE]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Type", elementKey: "type" },
{ label: "Enabled", elementKey: "enabled" },
]}
isLoading={loading}
records={records}
entityName="Keys"
idField="name"
/>
</SecureComponent>
</Grid>
</Grid>
</PageLayout>
</Fragment>
);
};
export default withStyles(styles)(IDPConfigurations);

View File

@@ -0,0 +1,48 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { ldapFormFields } from "./utils";
import LoginIcon from "@mui/icons-material/Login";
import IDPConfigurationDetails from "./IDPConfigurationDetails";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
type IDPLDAPConfigurationDetailsProps = {
classes?: any;
};
const styles = (theme: Theme) => createStyles({});
const IDPLDAPConfigurationDetails = ({
classes,
}: IDPLDAPConfigurationDetailsProps) => {
return (
<IDPConfigurationDetails
backLink={IAM_PAGES.IDP_LDAP_CONFIGURATIONS}
header={"LDAP Configurations"}
endpoint={"/api/v1/idp/ldap/"}
idpType={"ldap"}
formFields={ldapFormFields}
icon={<LoginIcon width={40} />}
/>
);
};
export default withStyles(styles)(IDPLDAPConfigurationDetails);

View File

@@ -0,0 +1,34 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import IDPConfigurations from "./IDPConfigurations";
type IDPLDAPConfigurationsProps = {
classes?: any;
};
const styles = (theme: Theme) => createStyles({});
const IDPLDAPConfigurations = ({ classes }: IDPLDAPConfigurationsProps) => {
return <IDPConfigurations idpType={"ldap"} />;
};
export default withStyles(styles)(IDPLDAPConfigurations);

View File

@@ -0,0 +1,48 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import { LockIcon } from "../../../icons";
import { openIDFormFields } from "./utils";
import IDPConfigurationDetails from "./IDPConfigurationDetails";
type IDPOpenIDConfigurationDetailsProps = {
classes?: any;
};
const styles = (theme: Theme) => createStyles({});
const IDPOpenIDConfigurationDetails = ({
classes,
}: IDPOpenIDConfigurationDetailsProps) => {
return (
<IDPConfigurationDetails
backLink={IAM_PAGES.IDP_OPENID_CONFIGURATIONS}
header={"OpenID Configurations"}
endpoint={"/api/v1/idp/openid/"}
idpType={"openid"}
formFields={openIDFormFields}
icon={<LockIcon width={40} />}
/>
);
};
export default withStyles(styles)(IDPOpenIDConfigurationDetails);

View File

@@ -0,0 +1,34 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import IDPConfigurations from "./IDPConfigurations";
type IDPOpenIDConfigurationsProps = {
classes?: any;
};
const styles = (theme: Theme) => createStyles({});
const IDPOpenIDConfigurations = ({ classes }: IDPOpenIDConfigurationsProps) => {
return <IDPConfigurations idpType={"openid"} />;
};
export default withStyles(styles)(IDPOpenIDConfigurations);

View File

@@ -0,0 +1,176 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 openIDFormFields = {
config_url: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Config URL is required" : "";
},
label: "Config URL",
tooltip: "Config URL for identity provider configuration",
placeholder:
"https://identity-provider-url/.well-known/openid-configuration",
type: "text",
},
client_id: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Client ID is required" : "";
},
label: "Client ID",
tooltip: "Identity provider Client ID",
placeholder: "Enter Client ID",
type: "text",
},
client_secret: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Client Secret is required" : "";
},
label: "Client Secret",
tooltip: "Identity provider Client Secret",
placeholder: "Enter Client Secret",
type: "password",
},
display_name: {
required: false,
label: "Display Name",
tooltip: "Display Name",
placeholder: "Enter Display Name",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
claim_name: {
required: false,
label: "Claim Name",
tooltip: "Claim from which MinIO will read the policy or role to use",
placeholder: "Enter Claim Name",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
claim_prefix: {
required: false,
label: "Claim Prefix",
tooltip: "Claim Prefix",
placeholder: "Enter Claim Prefix",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
scopes: {
required: false,
label: "Scopes",
tooltip: "Scopes",
placeholder: "openid,profile,email",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
redirect_uri: {
required: false,
label: "Redirect URI",
tooltip: "Redirect URI",
placeholder: "https://console-endpoint-url/oauth_callback",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
role_policy: {
required: false,
label: "Role Policy",
tooltip: "Role Policy",
placeholder: "readonly",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
};
export const ldapFormFields = {
server_addr: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Server Address is required" : "";
},
label: "Server Address",
tooltip: 'AD/LDAP server address e.g. "myldapserver.com:636"',
placeholder: "myldapserver.com:636",
type: "text",
},
lookup_bind_dn: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Lookup Bind DN is required" : "";
},
label: "Lookup Bind DN",
tooltip:
"DN for LDAP read-only service account used to perform DN and group lookups",
placeholder: "cn=admin,dc=min,dc=io",
type: "text",
},
lookup_bind_password: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "Lookup Bind Password is required" : "";
},
label: "Lookup Bind Password",
tooltip:
"Password for LDAP read-only service account used to perform DN and group lookups",
placeholder: "admin",
type: "password",
},
user_dn_search_base_dn: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "User DN Search Base DN is required" : "";
},
label: "User DN Search Base",
tooltip: "Base LDAP DN to search for user DN",
placeholder: "DC=example,DC=net",
type: "text",
},
user_dn_search_filter: {
required: true,
hasError: (s: string, editMode: boolean) => {
return !s && editMode ? "User DN Search Filter is required" : "";
},
label: "User DN Search Filter",
tooltip: "Search filter to lookup user DN",
placeholder: "(sAMAcountName=%s)",
type: "text",
},
display_name: {
required: false,
label: "Display Name",
tooltip: "Display Name",
placeholder: "Enter Display Name",
type: "text",
hasError: (s: string, editMode: boolean) => "",
},
group_search_base_dn: {
required: false,
hasError: (s: string, editMode: boolean) => "",
label: "Group Search Base DN",
tooltip: "Group Search Base DN",
placeholder: "ou=swengg,dc=min,dc=io",
type: "text",
},
group_search_filter: {
required: false,
hasError: (s: string, editMode: boolean) => "",
label: "Group Search Filter",
tooltip: "Group Search Filter",
placeholder: "(&(objectclass=groupofnames)(member=%d))",
type: "text",
},
};

View File

@@ -31,7 +31,6 @@ import {
IAM_SCOPES,
permissionTooltipHelper,
} from "../../../common/SecureComponent/permissions";
import { BucketObjectItem } from "../Buckets/ListBuckets/Objects/ListObjects/types";
import withSuspense from "../Common/Components/withSuspense";
import { setSnackBarMessage } from "../../../systemSlice";
import { AppState, useAppDispatch } from "../../../store";
@@ -58,7 +57,6 @@ interface IObjectBrowser {
bucketName: string;
internalPaths: string;
hidePathButton?: boolean;
existingFiles: BucketObjectItem[];
additionalOptions?: React.ReactNode;
}
@@ -66,7 +64,6 @@ const BrowserBreadcrumbs = ({
classes,
bucketName,
internalPaths,
existingFiles,
hidePathButton,
additionalOptions,
}: IObjectBrowser) => {
@@ -176,7 +173,6 @@ const BrowserBreadcrumbs = ({
bucketName={bucketName}
folderName={internalPaths}
onClose={closeAddFolderModal}
existingFiles={existingFiles}
/>
)}
<Grid item xs={12} className={`${classes.breadcrumbs}`}>

View File

@@ -16,6 +16,10 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { IFileItem, ObjectBrowserState } from "./types";
import {
BucketObjectItem,
IRestoreLocalObjectList,
} from "../Buckets/ListBuckets/Objects/ListObjects/types";
const defaultRewind = {
rewindEnabled: false,
@@ -47,6 +51,18 @@ const initialState: ObjectBrowserState = {
showDeleted: false,
selectedInternalPaths: null,
simplePath: null,
// object browser
records: [],
loadRecords: true,
loadingVersioning: true,
isVersioned: false,
lockingEnabled: false,
loadingLocking: false,
selectedObjects: [],
downloadRenameModal: null,
selectedPreview: null,
previewOpen: false,
shareFileModalOpen: false,
};
export const objectBrowserSlice = createSlice({
@@ -217,7 +233,7 @@ export const objectBrowserSlice = createSlice({
setSearchObjects: (state, action: PayloadAction<string>) => {
state.searchObjects = action.payload;
},
setLoadingObjectsList: (state, action: PayloadAction<boolean>) => {
setLoadingObjects: (state, action: PayloadAction<boolean>) => {
state.loadingObjects = action.payload;
},
setSearchVersions: (state, action: PayloadAction<string>) => {
@@ -259,6 +275,67 @@ export const objectBrowserSlice = createSlice({
action.payload,
];
},
setRecords: (state, action: PayloadAction<BucketObjectItem[]>) => {
state.records = action.payload;
},
setLoadingVersioning: (state, action: PayloadAction<boolean>) => {
state.loadingVersioning = action.payload;
},
setIsVersioned: (state, action: PayloadAction<boolean>) => {
state.isVersioned = action.payload;
},
setLockingEnabled: (state, action: PayloadAction<boolean>) => {
state.lockingEnabled = action.payload;
},
setLoadingLocking: (state, action: PayloadAction<boolean>) => {
state.loadingLocking = action.payload;
},
newMessage: (state, action: PayloadAction<BucketObjectItem[]>) => {
state.records = [...state.records, ...action.payload];
},
resetMessages: (state) => {
state.records = [];
},
setLoadingRecords: (state, action: PayloadAction<boolean>) => {
state.loadRecords = action.payload;
},
setSelectedObjects: (state, action: PayloadAction<string[]>) => {
state.selectedObjects = action.payload;
},
setDownloadRenameModal: (
state,
action: PayloadAction<BucketObjectItem | null>
) => {
state.downloadRenameModal = action.payload;
},
setSelectedPreview: (
state,
action: PayloadAction<BucketObjectItem | null>
) => {
state.selectedPreview = action.payload;
},
setPreviewOpen: (state, action: PayloadAction<boolean>) => {
state.previewOpen = action.payload;
},
setShareFileModalOpen: (state, action: PayloadAction<boolean>) => {
state.shareFileModalOpen = action.payload;
},
restoreLocalObjectList: (
state,
action: PayloadAction<IRestoreLocalObjectList>
) => {
const indexToReplace = state.records.findIndex(
(element) => element.name === action.payload.prefix
);
if (indexToReplace >= 0) {
state.records[indexToReplace].delete_flag =
action.payload.objectInfo.is_delete_marker;
state.records[indexToReplace].size = parseInt(
action.payload.objectInfo.size || "0"
);
}
},
},
});
export const {
@@ -275,7 +352,7 @@ export const {
openList,
closeList,
setSearchObjects,
setLoadingObjectsList,
setLoadingObjects,
cancelObjectInList,
setSearchVersions,
setSelectedVersion,
@@ -287,6 +364,20 @@ export const {
setSimplePathHandler,
newDownloadInit,
newUploadInit,
setRecords,
resetMessages,
setLoadingVersioning,
setIsVersioned,
setLoadingLocking,
setLockingEnabled,
newMessage,
setSelectedObjects,
setDownloadRenameModal,
setSelectedPreview,
setPreviewOpen,
setShareFileModalOpen,
setLoadingRecords,
restoreLocalObjectList,
} = objectBrowserSlice.actions;
export default objectBrowserSlice.reducer;

View File

@@ -0,0 +1,157 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { createAsyncThunk } from "@reduxjs/toolkit";
import { AppState } from "../../../store";
import { encodeURLString, getClientOS } from "../../../common/utils";
import { BucketObjectItem } from "../Buckets/ListBuckets/Objects/ListObjects/types";
import { makeid, storeCallForObjectWithID } from "./transferManager";
import { download } from "../Buckets/ListBuckets/Objects/utils";
import {
cancelObjectInList,
completeObject,
failObject,
setDownloadRenameModal,
setNewObject,
setPreviewOpen,
setSelectedPreview,
setShareFileModalOpen,
updateProgress,
} from "./objectBrowserSlice";
export const downloadSelected = createAsyncThunk(
"objectBrowser/downloadSelected",
async (bucketName: string, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState;
const downloadObject = (object: BucketObjectItem) => {
const identityDownload = encodeURLString(
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);
const ID = makeid(8);
const downloadCall = download(
bucketName,
encodeURLString(object.name),
object.version_id,
object.size,
null,
ID,
(progress) => {
dispatch(
updateProgress({
instanceID: identityDownload,
progress: progress,
})
);
},
() => {
dispatch(completeObject(identityDownload));
},
(msg: string) => {
dispatch(failObject({ instanceID: identityDownload, msg }));
},
() => {
dispatch(cancelObjectInList(identityDownload));
}
);
storeCallForObjectWithID(ID, downloadCall);
dispatch(
setNewObject({
ID,
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: object.name,
type: "download",
waitingForFile: true,
failed: false,
cancelled: false,
errorMessage: "",
})
);
};
if (state.objectBrowser.selectedObjects.length !== 0) {
let itemsToDownload: BucketObjectItem[] = [];
const filterFunction = (currValue: BucketObjectItem) =>
state.objectBrowser.selectedObjects.includes(currValue.name);
itemsToDownload = state.objectBrowser.records.filter(filterFunction);
// I case just one element is selected, then we trigger download modal validation.
// We are going to enforce zip download when multiple files are selected
if (itemsToDownload.length === 1) {
if (
itemsToDownload[0].name.length > 200 &&
getClientOS().toLowerCase().includes("win")
) {
dispatch(setDownloadRenameModal(itemsToDownload[0]));
return;
}
}
itemsToDownload.forEach((filteredItem) => {
downloadObject(filteredItem);
});
}
}
);
export const openPreview = createAsyncThunk(
"objectBrowser/openPreview",
async (_, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState;
if (state.objectBrowser.selectedObjects.length === 1) {
let fileObject: BucketObjectItem | undefined;
const findFunction = (currValue: BucketObjectItem) =>
state.objectBrowser.selectedObjects.includes(currValue.name);
fileObject = state.objectBrowser.records.find(findFunction);
if (fileObject) {
dispatch(setSelectedPreview(fileObject));
dispatch(setPreviewOpen(true));
}
}
}
);
export const openShare = createAsyncThunk(
"objectBrowser/openShare",
async (_, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState;
if (state.objectBrowser.selectedObjects.length === 1) {
let fileObject: BucketObjectItem | undefined;
const findFunction = (currValue: BucketObjectItem) =>
state.objectBrowser.selectedObjects.includes(currValue.name);
fileObject = state.objectBrowser.records.find(findFunction);
if (fileObject) {
dispatch(setSelectedPreview(fileObject));
dispatch(setShareFileModalOpen(true));
}
}
}
);

View File

@@ -14,6 +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 { BucketObjectItem } from "../Buckets/ListBuckets/Objects/ListObjects/types";
export const REWIND_SET_ENABLE = "REWIND/SET_ENABLE";
export const REWIND_RESET_REWIND = "REWIND/RESET_REWIND";
@@ -76,6 +78,17 @@ export interface ObjectBrowserState {
objectDetailsOpen: boolean;
selectedInternalPaths: string | null;
simplePath: string | null;
records: BucketObjectItem[];
loadRecords: boolean;
loadingVersioning: boolean;
isVersioned: boolean;
lockingEnabled: boolean;
loadingLocking: boolean;
selectedObjects: string[];
downloadRenameModal: BucketObjectItem | null;
selectedPreview: BucketObjectItem | null;
previewOpen: boolean;
shareFileModalOpen: boolean;
}
export interface ObjectManager {

View File

@@ -59,6 +59,7 @@ import {
import SettingsIcon from "../../icons/SettingsIcon";
import React from "react";
import LicenseBadge from "./Menu/LicenseBadge";
import { LockOpen, Login } from "@mui/icons-material";
export const validRoutes = (
features: string[] | null | undefined,
@@ -141,6 +142,20 @@ export const validRoutes = (
to: IAM_PAGES.POLICIES,
icon: AccessMenuIcon,
},
{
name: "OpenID",
component: NavLink,
id: "openID",
to: IAM_PAGES.IDP_OPENID_CONFIGURATIONS,
icon: LockOpen,
},
{
name: "LDAP",
component: NavLink,
id: "ldap",
to: IAM_PAGES.IDP_LDAP_CONFIGURATIONS,
icon: Login,
},
],
},

View File

@@ -142,6 +142,9 @@ const LoginCallback = ({ classes }: ILoginCallBackProps) => {
targetPath = `${localStorage.getItem("redirect-path")}`;
localStorage.setItem("redirect-path", "");
}
if (state) {
localStorage.setItem("auth-state", state);
}
setLoading(false);
navigate(targetPath);
})

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