Compare commits

...

14 Commits

Author SHA1 Message Date
Daniel Valdivia
2cae87aaed Release v0.10.4 (#1108)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-10-12 16:41:25 -07:00
Harshavardhana
b82441813d update to new minio/pkg v1.1.5 (#1107)
this update fixes dropping valid statements
as duplicates during iampolicy.ParseConfig()

fixes situations when users have overlapping
policies, then server should apply both
policies together.
2021-10-12 16:21:13 -07:00
Daniel Valdivia
5dfba3f6c8 Fix Broken Hop Cookies (#1106)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-10-12 14:35:12 -07:00
jinapurapu
1b9902a5be Prevent adding user with access key already in use (#1103)
* Release v0.10.3 (#1098)

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>

WIP check if accesskey exists before adding user

* Added error when duplicate access key attempted

* Removed unneeded code

* Changed api to getUserInfo

* Corrected error messages

Co-authored-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-10-12 11:18:56 -07:00
Alex
d6944ccd3b Added zoom option to line charts & bar charts in prometheus dashboard (#1104) 2021-10-11 19:17:18 -07:00
Alex
ebaa1947de Changed number representations in Prometheus dashboard (#1101)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
2021-10-11 12:32:27 -07:00
Daniel Valdivia
9d61af7060 Release v0.10.3 (#1098)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-10-05 13:00:12 -07:00
Daniel Valdivia
1b225e0901 fix: broken STS Sessions with large policies (#1096)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-10-04 14:25:00 -07:00
Alex
7a864d2631 Changed error modal snackbar (#1093)
Changed error modal snackbar to use a simplified style of global error snackbar. also fixed an issue where error was persistent if you closed the modalbox with an error present

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2021-09-30 09:25:34 -07:00
Daniel Valdivia
dfca19092a Dashboard Tweaks (#1091)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-09-28 16:36:59 -07:00
Alex
61cf397a02 Loaded correct version of the file for sharing (#1090)
Loaded correct version of the file for sharing when undefined is received in share window, this fixes an issue with objects list where non version was retrieved from backend

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-09-28 16:28:02 -07:00
Daniel Valdivia
d31528e2b5 Uptime Icon (#1088)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>

Co-authored-by: Alex <33497058+bexsoft@users.noreply.github.com>
2021-09-28 16:23:25 -07:00
Alex
8fd1e0db9c Fixed widgets overlaps & some style adjustments in Dashboard (#1087)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
2021-09-28 14:51:38 -05:00
Lenin Alevski
3d27cd2bd3 Multiple fixes for sub path and objects filename encoding (#1086)
- fix: objects with special characters (ie: /,&,%,*) won't open
- fix: create subdolders with special characters won't work, ie: /,&,%,*
- fix: view subfolders with special characters (ie: /,&,%,*) won't work
- refactor: browser breadcrumb
- fix: rewind enable/disable toggle button not working
- fix: undefined style for add bucket button in buckets page
- Added: validation for folder path naming
- refactor: encode prefix parameter using base64 to avoid url encode
  issues
- fix: share link for versioned object won't work because of wrong
  version_id

Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
2021-09-28 12:25:28 -07:00
62 changed files with 1493 additions and 481 deletions

View File

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

View File

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

2
go.mod
View File

@@ -23,7 +23,7 @@ require (
github.com/minio/minio-go/v7 v7.0.14
github.com/minio/operator v0.0.0-20210812082324-26350f153661
github.com/minio/operator/logsearchapi v0.0.0-20210812082324-26350f153661
github.com/minio/pkg v1.1.3
github.com/minio/pkg v1.1.5
github.com/minio/selfupdate v0.3.1
github.com/mitchellh/go-homedir v1.1.0
github.com/rs/xid v1.2.1

4
go.sum
View File

@@ -893,8 +893,8 @@ github.com/minio/operator/logsearchapi v0.0.0-20210812082324-26350f153661/go.mod
github.com/minio/pkg v1.0.3/go.mod h1:obU54TZ9QlMv0TRaDgQ/JTzf11ZSXxnSfLrm4tMtBP8=
github.com/minio/pkg v1.0.4/go.mod h1:obU54TZ9QlMv0TRaDgQ/JTzf11ZSXxnSfLrm4tMtBP8=
github.com/minio/pkg v1.0.8/go.mod h1:32x/3OmGB0EOi1N+3ggnp+B5VFkSBBB9svPMVfpnf14=
github.com/minio/pkg v1.1.3 h1:J4vGnlNSxc/o9gDOQMZ3k0L3koA7ZgBQ7GRMrUpt/OY=
github.com/minio/pkg v1.1.3/go.mod h1:32x/3OmGB0EOi1N+3ggnp+B5VFkSBBB9svPMVfpnf14=
github.com/minio/pkg v1.1.5 h1:phwKkJBQdVLyxOXC3RChPVGLtebplzQJ5jJ3l/HBvnk=
github.com/minio/pkg v1.1.5/go.mod h1:32x/3OmGB0EOi1N+3ggnp+B5VFkSBBB9svPMVfpnf14=
github.com/minio/selfupdate v0.3.1 h1:BWEFSNnrZVMUWXbXIgLDNDjbejkmpAmZvy/nCz1HlEs=
github.com/minio/selfupdate v0.3.1/go.mod h1:b8ThJzzH7u2MkF6PcIra7KaXO9Khf6alWPvMSyTDCFM=
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=

View File

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

View File

@@ -55,8 +55,10 @@ func registerLoginHandlers(api *operations.OperatorAPI) {
}
// Custom response writer to set the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
cookie := restapi.NewSessionCookieForConsole(loginResponse.SessionID)
http.SetCookie(w, &cookie)
cookies := restapi.NewSessionCookieForConsole(loginResponse.SessionID)
for _, cookie := range cookies {
http.SetCookie(w, &cookie)
}
user_api.NewLoginCreated().WithPayload(loginResponse).WriteResponse(w, p)
})
})
@@ -67,8 +69,10 @@ func registerLoginHandlers(api *operations.OperatorAPI) {
}
// Custom response writer to set the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
cookie := restapi.NewSessionCookieForConsole(loginResponse.SessionID)
http.SetCookie(w, &cookie)
cookies := restapi.NewSessionCookieForConsole(loginResponse.SessionID)
for _, cookie := range cookies {
http.SetCookie(w, &cookie)
}
user_api.NewLoginOauth2AuthCreated().WithPayload(loginResponse).WriteResponse(w, p)
})
})
@@ -79,8 +83,10 @@ func registerLoginHandlers(api *operations.OperatorAPI) {
}
// Custom response writer to set the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
cookie := restapi.NewSessionCookieForConsole(loginResponse.SessionID)
http.SetCookie(w, &cookie)
cookies := restapi.NewSessionCookieForConsole(loginResponse.SessionID)
for _, cookie := range cookies {
http.SetCookie(w, &cookie)
}
user_api.NewLoginOperatorCreated().WithPayload(loginResponse).WriteResponse(w, p)
})
})

View File

@@ -287,6 +287,7 @@ func decrypt(ciphertext []byte, associatedData []byte) ([]byte, error) {
func GetTokenFromRequest(r *http.Request) (string, error) {
// Token might come either as a Cookie or as a Header
// if not set in cookie, check if it is set on Header.
tokenCookie, err := r.Cookie("token")
if err != nil {
return "", ErrNoAuthToken
@@ -295,7 +296,17 @@ func GetTokenFromRequest(r *http.Request) (string, error) {
if tokenCookie.Expires.After(currentTime) {
return "", errTokenExpired
}
return strings.TrimSpace(tokenCookie.Value), nil
mergeToken := strings.TrimSpace(tokenCookie.Value)
for _, cookie := range r.Cookies() {
// any cookie with token%d structure
if cookie.Name != "token" && !strings.HasPrefix(cookie.Name, "token-") && strings.HasPrefix(cookie.Name, "token") {
mergeToken = fmt.Sprintf("%s%s", mergeToken, strings.TrimSpace(cookie.Value))
}
}
return mergeToken, nil
}
func GetClaimsFromTokenInRequest(req *http.Request) (*models.Principal, error) {

View File

@@ -1,23 +1,23 @@
{
"files": {
"main.css": "./static/css/main.96c516b4.chunk.css",
"main.js": "./static/js/main.cbf18a78.chunk.js",
"main.js.map": "./static/js/main.cbf18a78.chunk.js.map",
"main.css": "./static/css/main.e33a67ba.chunk.css",
"main.js": "./static/js/main.e81f26fe.chunk.js",
"main.js.map": "./static/js/main.e81f26fe.chunk.js.map",
"runtime-main.js": "./static/js/runtime-main.30f8243a.js",
"runtime-main.js.map": "./static/js/runtime-main.30f8243a.js.map",
"static/css/2.cb056e1c.chunk.css": "./static/css/2.cb056e1c.chunk.css",
"static/js/2.d2f64bf3.chunk.js": "./static/js/2.d2f64bf3.chunk.js",
"static/js/2.d2f64bf3.chunk.js.map": "./static/js/2.d2f64bf3.chunk.js.map",
"static/css/2.5b1f144e.chunk.css": "./static/css/2.5b1f144e.chunk.css",
"static/js/2.dd760fd2.chunk.js": "./static/js/2.dd760fd2.chunk.js",
"static/js/2.dd760fd2.chunk.js.map": "./static/js/2.dd760fd2.chunk.js.map",
"index.html": "./index.html",
"static/css/2.cb056e1c.chunk.css.map": "./static/css/2.cb056e1c.chunk.css.map",
"static/css/main.96c516b4.chunk.css.map": "./static/css/main.96c516b4.chunk.css.map",
"static/js/2.d2f64bf3.chunk.js.LICENSE.txt": "./static/js/2.d2f64bf3.chunk.js.LICENSE.txt"
"static/css/2.5b1f144e.chunk.css.map": "./static/css/2.5b1f144e.chunk.css.map",
"static/css/main.e33a67ba.chunk.css.map": "./static/css/main.e33a67ba.chunk.css.map",
"static/js/2.dd760fd2.chunk.js.LICENSE.txt": "./static/js/2.dd760fd2.chunk.js.LICENSE.txt"
},
"entrypoints": [
"static/js/runtime-main.30f8243a.js",
"static/css/2.cb056e1c.chunk.css",
"static/js/2.d2f64bf3.chunk.js",
"static/css/main.96c516b4.chunk.css",
"static/js/main.cbf18a78.chunk.js"
"static/css/2.5b1f144e.chunk.css",
"static/js/2.dd760fd2.chunk.js",
"static/css/main.e33a67ba.chunk.css",
"static/js/main.e81f26fe.chunk.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#081C42" media="(prefers-color-scheme: light)"/><meta name="theme-color" content="#081C42" media="(prefers-color-scheme: dark)"/><meta name="description" content="MinIO Console"/><link href="./styles/root-styles.css" rel="stylesheet"/><link rel="apple-touch-icon" sizes="180x180" href="./apple-icon-180x180.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"/><link rel="icon" type="image/png" sizes="96x96" href="./favicon-96x96.png"/><link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png"/><link rel="manifest" href="./manifest.json"/><link rel="mask-icon" href="./safari-pinned-tab.svg" color="#3a4e54"/><title>MinIO Console</title><link href="./static/css/2.cb056e1c.chunk.css" rel="stylesheet"><link href="./static/css/main.96c516b4.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="loader-block"><svg class="loader-svg-container" viewBox="22 22 44 44"><circle class="loader-style MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate" cx="44" cy="44" r="20.2" fill="none" stroke-width="3.6"></circle></svg></div></div><script>!function(e){function r(r){for(var n,l,i=r[0],a=r[1],p=r[2],c=0,s=[];c<i.length;c++)l=i[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,p||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var a=t[i];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="./";var i=this["webpackJsonpportal-ui"]=this["webpackJsonpportal-ui"]||[],a=i.push.bind(i);i.push=r,i=i.slice();for(var p=0;p<i.length;p++)r(i[p]);var f=a;t()}([])</script><script src="./static/js/2.d2f64bf3.chunk.js"></script><script src="./static/js/main.cbf18a78.chunk.js"></script></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#081C42" media="(prefers-color-scheme: light)"/><meta name="theme-color" content="#081C42" media="(prefers-color-scheme: dark)"/><meta name="description" content="MinIO Console"/><link href="./styles/root-styles.css" rel="stylesheet"/><link rel="apple-touch-icon" sizes="180x180" href="./apple-icon-180x180.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"/><link rel="icon" type="image/png" sizes="96x96" href="./favicon-96x96.png"/><link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png"/><link rel="manifest" href="./manifest.json"/><link rel="mask-icon" href="./safari-pinned-tab.svg" color="#3a4e54"/><title>MinIO Console</title><link href="./static/css/2.5b1f144e.chunk.css" rel="stylesheet"><link href="./static/css/main.e33a67ba.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="loader-block"><svg class="loader-svg-container" viewBox="22 22 44 44"><circle class="loader-style MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate" cx="44" cy="44" r="20.2" fill="none" stroke-width="3.6"></circle></svg></div></div><script>!function(e){function r(r){for(var n,l,i=r[0],a=r[1],p=r[2],c=0,s=[];c<i.length;c++)l=i[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,p||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var a=t[i];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="./";var i=this["webpackJsonpportal-ui"]=this["webpackJsonpportal-ui"]||[],a=i.push.bind(i);i.push=r,i=i.slice();for(var p=0;p<i.length;p++)r(i[p]);var f=a;t()}([])</script><script src="./static/js/2.dd760fd2.chunk.js"></script><script src="./static/js/main.e81f26fe.chunk.js"></script></body></html>

View File

@@ -1,2 +1,2 @@
.ReactVirtualized__Table__headerRow{font-weight:700;text-transform:uppercase}.ReactVirtualized__Table__headerRow,.ReactVirtualized__Table__row{display:flex;flex-direction:row;align-items:center}.ReactVirtualized__Table__headerTruncatedText{display:inline-block;max-width:100%;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.ReactVirtualized__Table__headerColumn,.ReactVirtualized__Table__rowColumn{margin-right:10px;min-width:0}.ReactVirtualized__Table__rowColumn{text-overflow:ellipsis;white-space:nowrap}.ReactVirtualized__Table__headerColumn:first-of-type,.ReactVirtualized__Table__rowColumn:first-of-type{margin-left:10px}.ReactVirtualized__Table__sortableHeaderColumn{cursor:pointer}.ReactVirtualized__Table__sortableHeaderIconContainer{display:flex;align-items:center}.ReactVirtualized__Table__sortableHeaderIcon{flex:0 0 24px;height:1em;width:1em;fill:currentColor}.react-grid-layout{position:relative;transition:height .2s ease}.react-grid-item{transition:all .2s ease;transition-property:left,top}.react-grid-item img{pointer-events:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.react-grid-item.cssTransforms{transition-property:-webkit-transform;transition-property:transform;transition-property:transform,-webkit-transform}.react-grid-item.resizing{z-index:1;will-change:width,height}.react-grid-item.react-draggable-dragging{transition:none;z-index:3;will-change:transform}.react-grid-item.dropping{visibility:hidden}.react-grid-item.react-grid-placeholder{background:red;opacity:.2;transition-duration:.1s;z-index:2;-webkit-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.react-grid-item>.react-resizable-handle{position:absolute;width:20px;height:20px}.react-grid-item>.react-resizable-handle:after{content:"";position:absolute;right:3px;bottom:3px;width:5px;height:5px;border-right:2px solid rgba(0,0,0,.4);border-bottom:2px solid rgba(0,0,0,.4)}.react-resizable-hide>.react-resizable-handle{display:none}.react-grid-item>.react-resizable-handle.react-resizable-handle-sw{bottom:0;left:0;cursor:sw-resize;-webkit-transform:rotate(90deg);transform:rotate(90deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-se{bottom:0;right:0;cursor:se-resize}.react-grid-item>.react-resizable-handle.react-resizable-handle-nw{top:0;left:0;cursor:nw-resize;-webkit-transform:rotate(180deg);transform:rotate(180deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-ne{top:0;right:0;cursor:ne-resize;-webkit-transform:rotate(270deg);transform:rotate(270deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-e,.react-grid-item>.react-resizable-handle.react-resizable-handle-w{top:50%;margin-top:-10px;cursor:ew-resize}.react-grid-item>.react-resizable-handle.react-resizable-handle-w{left:0;-webkit-transform:rotate(135deg);transform:rotate(135deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-e{right:0;-webkit-transform:rotate(315deg);transform:rotate(315deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-n,.react-grid-item>.react-resizable-handle.react-resizable-handle-s{left:50%;margin-left:-10px;cursor:ns-resize}.react-grid-item>.react-resizable-handle.react-resizable-handle-n{top:0;-webkit-transform:rotate(225deg);transform:rotate(225deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-s{bottom:0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}.react-resizable{position:relative}.react-resizable-handle{position:absolute;width:20px;height:20px;background-repeat:no-repeat;background-origin:content-box;box-sizing:border-box;background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgd2lkdGg9IjYiIGhlaWdodD0iNiI+PHBhdGggZD0iTTYgNkgwVjQuMmg0LjJWMEg2djZ6IiBvcGFjaXR5PSIuMzAyIi8+PC9zdmc+");background-position:100% 100%;padding:0 3px 3px 0}.react-resizable-handle-sw{bottom:0;left:0;cursor:sw-resize;-webkit-transform:rotate(90deg);transform:rotate(90deg)}.react-resizable-handle-se{bottom:0;right:0;cursor:se-resize}.react-resizable-handle-nw{top:0;left:0;cursor:nw-resize;-webkit-transform:rotate(180deg);transform:rotate(180deg)}.react-resizable-handle-ne{top:0;right:0;cursor:ne-resize;-webkit-transform:rotate(270deg);transform:rotate(270deg)}.react-resizable-handle-e,.react-resizable-handle-w{top:50%;margin-top:-10px;cursor:ew-resize}.react-resizable-handle-w{left:0;-webkit-transform:rotate(135deg);transform:rotate(135deg)}.react-resizable-handle-e{right:0;-webkit-transform:rotate(315deg);transform:rotate(315deg)}.react-resizable-handle-n,.react-resizable-handle-s{left:50%;margin-left:-10px;cursor:ns-resize}.react-resizable-handle-n{top:0;-webkit-transform:rotate(225deg);transform:rotate(225deg)}.react-resizable-handle-s{bottom:0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}
/*# sourceMappingURL=2.cb056e1c.chunk.css.map */
/*# sourceMappingURL=2.5b1f144e.chunk.css.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

View File

@@ -1,130 +1,135 @@
/*Lato Font import*/
@font-face {
font-family: 'Lato';
src: url('./fonts/Lato/Lato-BlackItalic.eot');
src: url('./fonts/Lato/Lato-BlackItalic.eot?#iefix') format('embedded-opentype'),
url('./fonts/Lato/Lato-BlackItalic.woff2') format('woff2'),
url('./fonts/Lato/Lato-BlackItalic.woff') format('woff'),
url('./fonts/Lato/Lato-BlackItalic.ttf') format('truetype'),
url('./fonts/Lato/Lato-BlackItalic.svg#Lato-BlackItalic') format('svg');
font-family: "Lato";
src: url("./fonts/Lato/Lato-BlackItalic.eot");
src: url("./fonts/Lato/Lato-BlackItalic.eot?#iefix")
format("embedded-opentype"),
url("./fonts/Lato/Lato-BlackItalic.woff2") format("woff2"),
url("./fonts/Lato/Lato-BlackItalic.woff") format("woff"),
url("./fonts/Lato/Lato-BlackItalic.ttf") format("truetype"),
url("./fonts/Lato/Lato-BlackItalic.svg#Lato-BlackItalic") format("svg");
font-weight: 900;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Lato';
src: url('./fonts/Lato/Lato-Bold.eot');
src: url('./fonts/Lato/Lato-Bold.eot?#iefix') format('embedded-opentype'),
url('./fonts/Lato/Lato-Bold.woff2') format('woff2'),
url('./fonts/Lato/Lato-Bold.woff') format('woff'),
url('./fonts/Lato/Lato-Bold.ttf') format('truetype'),
url('./fonts/Lato/Lato-Bold.svg#Lato-Bold') format('svg');
font-family: "Lato";
src: url("./fonts/Lato/Lato-Bold.eot");
src: url("./fonts/Lato/Lato-Bold.eot?#iefix") format("embedded-opentype"),
url("./fonts/Lato/Lato-Bold.woff2") format("woff2"),
url("./fonts/Lato/Lato-Bold.woff") format("woff"),
url("./fonts/Lato/Lato-Bold.ttf") format("truetype"),
url("./fonts/Lato/Lato-Bold.svg#Lato-Bold") format("svg");
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Lato';
src: url('./fonts/Lato/Lato-BoldItalic.eot');
src: url('./fonts/Lato/Lato-BoldItalic.eot?#iefix') format('embedded-opentype'),
url('./fonts/Lato/Lato-BoldItalic.woff2') format('woff2'),
url('./fonts/Lato/Lato-BoldItalic.woff') format('woff'),
url('./fonts/Lato/Lato-BoldItalic.ttf') format('truetype'),
url('./fonts/Lato/Lato-BoldItalic.svg#Lato-BoldItalic') format('svg');
font-family: "Lato";
src: url("./fonts/Lato/Lato-BoldItalic.eot");
src: url("./fonts/Lato/Lato-BoldItalic.eot?#iefix")
format("embedded-opentype"),
url("./fonts/Lato/Lato-BoldItalic.woff2") format("woff2"),
url("./fonts/Lato/Lato-BoldItalic.woff") format("woff"),
url("./fonts/Lato/Lato-BoldItalic.ttf") format("truetype"),
url("./fonts/Lato/Lato-BoldItalic.svg#Lato-BoldItalic") format("svg");
font-weight: bold;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Lato';
src: url('./fonts/Lato/Lato-Light.eot');
src: url('./fonts/Lato/Lato-Light.eot?#iefix') format('embedded-opentype'),
url('./fonts/Lato/Lato-Light.woff2') format('woff2'),
url('./fonts/Lato/Lato-Light.woff') format('woff'),
url('./fonts/Lato/Lato-Light.ttf') format('truetype'),
url('./fonts/Lato/Lato-Light.svg#Lato-Light') format('svg');
font-family: "Lato";
src: url("./fonts/Lato/Lato-Light.eot");
src: url("./fonts/Lato/Lato-Light.eot?#iefix") format("embedded-opentype"),
url("./fonts/Lato/Lato-Light.woff2") format("woff2"),
url("./fonts/Lato/Lato-Light.woff") format("woff"),
url("./fonts/Lato/Lato-Light.ttf") format("truetype"),
url("./fonts/Lato/Lato-Light.svg#Lato-Light") format("svg");
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Lato';
src: url('./fonts/Lato/Lato-Black.eot');
src: url('./fonts/Lato/Lato-Black.eot?#iefix') format('embedded-opentype'),
url('./fonts/Lato/Lato-Black.woff2') format('woff2'),
url('./fonts/Lato/Lato-Black.woff') format('woff'),
url('./fonts/Lato/Lato-Black.ttf') format('truetype'),
url('./fonts/Lato/Lato-Black.svg#Lato-Black') format('svg');
font-family: "Lato";
src: url("./fonts/Lato/Lato-Black.eot");
src: url("./fonts/Lato/Lato-Black.eot?#iefix") format("embedded-opentype"),
url("./fonts/Lato/Lato-Black.woff2") format("woff2"),
url("./fonts/Lato/Lato-Black.woff") format("woff"),
url("./fonts/Lato/Lato-Black.ttf") format("truetype"),
url("./fonts/Lato/Lato-Black.svg#Lato-Black") format("svg");
font-weight: 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Lato';
src: url('./fonts/Lato/Lato-Italic.eot');
src: url('./fonts/Lato/Lato-Italic.eot?#iefix') format('embedded-opentype'),
url('./fonts/Lato/Lato-Italic.woff2') format('woff2'),
url('./fonts/Lato/Lato-Italic.woff') format('woff'),
url('./fonts/Lato/Lato-Italic.ttf') format('truetype'),
url('./fonts/Lato/Lato-Italic.svg#Lato-Italic') format('svg');
font-family: "Lato";
src: url("./fonts/Lato/Lato-Italic.eot");
src: url("./fonts/Lato/Lato-Italic.eot?#iefix") format("embedded-opentype"),
url("./fonts/Lato/Lato-Italic.woff2") format("woff2"),
url("./fonts/Lato/Lato-Italic.woff") format("woff"),
url("./fonts/Lato/Lato-Italic.ttf") format("truetype"),
url("./fonts/Lato/Lato-Italic.svg#Lato-Italic") format("svg");
font-weight: normal;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Lato Hairline';
src: url('./fonts/Lato/Lato-Hairline.eot');
src: url('./fonts/Lato/Lato-Hairline.eot?#iefix') format('embedded-opentype'),
url('./fonts/Lato/Lato-Hairline.woff2') format('woff2'),
url('./fonts/Lato/Lato-Hairline.woff') format('woff'),
url('./fonts/Lato/Lato-Hairline.ttf') format('truetype'),
url('./fonts/Lato/Lato-Hairline.svg#Lato-Hairline') format('svg');
font-family: "Lato Hairline";
src: url("./fonts/Lato/Lato-Hairline.eot");
src: url("./fonts/Lato/Lato-Hairline.eot?#iefix") format("embedded-opentype"),
url("./fonts/Lato/Lato-Hairline.woff2") format("woff2"),
url("./fonts/Lato/Lato-Hairline.woff") format("woff"),
url("./fonts/Lato/Lato-Hairline.ttf") format("truetype"),
url("./fonts/Lato/Lato-Hairline.svg#Lato-Hairline") format("svg");
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Lato';
src: url('./fonts/Lato/Lato-Regular.eot');
src: url('./fonts/Lato/Lato-Regular.eot?#iefix') format('embedded-opentype'),
url('./fonts/Lato/Lato-Regular.woff2') format('woff2'),
url('./fonts/Lato/Lato-Regular.woff') format('woff'),
url('./fonts/Lato/Lato-Regular.ttf') format('truetype'),
url('./fonts/Lato/Lato-Regular.svg#Lato-Regular') format('svg');
font-family: "Lato";
src: url("./fonts/Lato/Lato-Regular.eot");
src: url("./fonts/Lato/Lato-Regular.eot?#iefix") format("embedded-opentype"),
url("./fonts/Lato/Lato-Regular.woff2") format("woff2"),
url("./fonts/Lato/Lato-Regular.woff") format("woff"),
url("./fonts/Lato/Lato-Regular.ttf") format("truetype"),
url("./fonts/Lato/Lato-Regular.svg#Lato-Regular") format("svg");
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Lato Hairline';
src: url('./fonts/Lato/Lato-HairlineItalic.eot');
src: url('./fonts/Lato/Lato-HairlineItalic.eot?#iefix') format('embedded-opentype'),
url('./fonts/Lato/Lato-HairlineItalic.woff2') format('woff2'),
url('./fonts/Lato/Lato-HairlineItalic.woff') format('woff'),
url('./fonts/Lato/Lato-HairlineItalic.ttf') format('truetype'),
url('./fonts/Lato/Lato-HairlineItalic.svg#Lato-HairlineItalic') format('svg');
font-family: "Lato Hairline";
src: url("./fonts/Lato/Lato-HairlineItalic.eot");
src: url("./fonts/Lato/Lato-HairlineItalic.eot?#iefix")
format("embedded-opentype"),
url("./fonts/Lato/Lato-HairlineItalic.woff2") format("woff2"),
url("./fonts/Lato/Lato-HairlineItalic.woff") format("woff"),
url("./fonts/Lato/Lato-HairlineItalic.ttf") format("truetype"),
url("./fonts/Lato/Lato-HairlineItalic.svg#Lato-HairlineItalic")
format("svg");
font-weight: 300;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Lato';
src: url('./fonts/Lato/Lato-LightItalic.eot');
src: url('./fonts/Lato/Lato-LightItalic.eot?#iefix') format('embedded-opentype'),
url('./fonts/Lato/Lato-LightItalic.woff2') format('woff2'),
url('./fonts/Lato/Lato-LightItalic.woff') format('woff'),
url('./fonts/Lato/Lato-LightItalic.ttf') format('truetype'),
url('./fonts/Lato/Lato-LightItalic.svg#Lato-LightItalic') format('svg');
font-family: "Lato";
src: url("./fonts/Lato/Lato-LightItalic.eot");
src: url("./fonts/Lato/Lato-LightItalic.eot?#iefix")
format("embedded-opentype"),
url("./fonts/Lato/Lato-LightItalic.woff2") format("woff2"),
url("./fonts/Lato/Lato-LightItalic.woff") format("woff"),
url("./fonts/Lato/Lato-LightItalic.ttf") format("truetype"),
url("./fonts/Lato/Lato-LightItalic.svg#Lato-LightItalic") format("svg");
font-weight: 300;
font-style: italic;
font-display: swap;

View File

@@ -76,6 +76,9 @@ export const deleteCookie = (name: string) => {
export const clearSession = () => {
storage.removeItem("token");
deleteCookie("token");
for (let i = 1; i < 10; i++) {
deleteCookie(`token${i}`);
}
};
// timeFromDate gets time string from date input
@@ -556,3 +559,29 @@ export const prettyNumber = (usage: number | undefined) => {
return usage.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
export const representationNumber = (number: number | undefined) => {
if (number === undefined) {
return "0";
}
let returnValue = number.toString();
let unit = "";
if (number > 999 && number < 1000000) {
returnValue = (number / 1000).toFixed(1); // convert to K, numbers > 999
unit = "K";
} else if (number >= 1000000 && number < 1000000000) {
returnValue = (number / 1000000).toFixed(1); // convert to M, numbers >= 1 million
unit = "M";
} else if (number >= 1000000000) {
returnValue = (number / 1000000000).toFixed(1); // convert to B, numbers >= 1 billion
unit = "B";
}
if (returnValue.endsWith(".0")) {
returnValue = returnValue.slice(0, -2);
}
return `${returnValue}${unit}`;
};

View File

@@ -0,0 +1,38 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import * as React from "react";
import { SvgIcon, SvgIconProps } from "@material-ui/core";
const UptimeIcon = (props: SvgIconProps) => {
return (
<SvgIcon {...props}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 206.043 254.417">
<path
data-name="Sustracci\xF3n 3"
d="M194.711 153.049H184.19c.012-.6.016-1.138.016-1.653a81.643 81.643 0 00-1.061-13.146h-64.129V60.127l25.357 11.174 44.693 33.842 4.191 33.167a52.729 52.729 0 015.316 4.4 22 22 0 013.371 3.923c.928 1.466 1.24 2.7.932 3.672-.575 1.822-3.321 2.744-8.165 2.744z"
fill="#e3e3e3"
/>
<path
data-name="Uni\xF3n 9"
d="M0 151.401a102.413 102.413 0 016.553-36.173 102.779 102.779 0 0118.063-30.583 103.552 103.552 0 0112.6-12.447 103.819 103.819 0 0114.568-10.149 102.151 102.151 0 0133.875-12.207l.58-.1v22.724l-.393.088a80.116 80.116 0 00-25.221 10.222 81.119 81.119 0 00-20.129 17.684 80.667 80.667 0 00-13.328 23.446 80.291 80.291 0 00-4.822 27.494 80.772 80.772 0 0080.682 80.678 80.772 80.772 0 0080.684-80.678 80.257 80.257 0 00-4.957-27.862 80.6 80.6 0 00-13.686-23.672 81.1 81.1 0 00-20.631-17.694 79.844 79.844 0 00-25.793-9.942l-.4-.083v-22.65l.576.088a101.976 101.976 0 0134.443 11.887 104.181 104.181 0 0114.84 10.109 105.131 105.131 0 0112.836 12.477 102.82 102.82 0 0118.416 30.8 102.374 102.374 0 016.7 36.542 103.136 103.136 0 01-103.02 103.018A103.137 103.137 0 010 151.401zm103.584 9.849a9.94 9.94 0 01-1.012-.054c-4.676-.093-9.285-3.011-9.285-8.749V30.28L82.496 40.331c-8.852 8.248-22.311-4.3-13.459-12.541L96.014 2.649a10.033 10.033 0 0113.582 0l26.982 25.141c8.854 8.243-4.611 20.789-13.469 12.541L112.328 30.28v112.971h41a9 9 0 019 9 9 9 0 01-9 8.994z"
/>
</svg>
</SvgIcon>
);
};
export default UptimeIcon;

View File

@@ -98,3 +98,4 @@ export { default as FileVideoIcon } from "./FileVideoIcon";
export { default as ArrowRightIcon } from "./ArrowRightIcon";
export { default as CalendarIcon } from "./CalendarIcon";
export { default as UptimeIcon } from "./UptimeIcon";

View File

@@ -88,6 +88,9 @@ const styles = (theme: Theme) =>
theaderSearchLabel: {
color: theme.palette.grey["400"],
},
addBucket: {
marginRight: 8,
},
theaderSearch: {
borderColor: theme.palette.grey["200"],
"& .MuiInputBase-input": {
@@ -102,9 +105,6 @@ const styles = (theme: Theme) =>
},
},
},
addBucket: {
marginRight: 8,
},
actionHeaderItems: {
"@media (min-width: 320px)": {
marginTop: 8,

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
import { Button, Grid } from "@material-ui/core";
import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
@@ -54,24 +54,51 @@ const CreateFolderModal = ({
classes,
}: ICreateFolder) => {
const [pathUrl, setPathUrl] = useState("");
const [nameInputError, setNameInputError] = useState<string>("");
const [isFormValid, setIsFormValid] = useState<boolean>(false);
const currentPath = `${bucketName}/${folderName}`;
const currentPath = `${bucketName}/${atob(folderName)}`;
const resetForm = () => {
setPathUrl("");
};
const createProcess = () => {
const newPath = `/buckets/${bucketName}/browse/${
folderName !== "" ? `${folderName}/` : ""
}${pathUrl}`;
let folderPath = "";
if (folderName !== "") {
const decodedFolderName = atob(folderName);
folderPath = decodedFolderName.endsWith("/")
? decodedFolderName
: `${decodedFolderName}/`;
}
const newPath = `/buckets/${bucketName}/browse/${btoa(
`${folderPath}${pathUrl}`
)}/`;
history.push(newPath);
setFileModeEnabled(false);
onClose();
};
const validPathURL = useCallback(() => {
const patternAgainst = /^[a-zA-Z0-9*'#-\[\]_/&.@\s()]+$/; // Only allow uppercase, numbers, dashes and underscores
if (patternAgainst.test(pathUrl)) {
setNameInputError("");
return true;
}
setNameInputError(
"Please verify the folder path contains valid characters only (letters, numbers and some special characters)."
);
return false;
}, [pathUrl]);
useEffect(() => {
let valid = true;
if (pathUrl.trim().length === 0 || !validPathURL()) {
valid = false;
}
setIsFormValid(valid);
}, [pathUrl]);
return (
<React.Fragment>
<ModalWrapper
@@ -91,6 +118,8 @@ const CreateFolderModal = ({
onChange={(e) => {
setPathUrl(e.target.value);
}}
required
error={nameInputError}
/>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
@@ -106,7 +135,7 @@ const CreateFolderModal = ({
type="submit"
variant="contained"
color="primary"
disabled={pathUrl.trim() === ""}
disabled={!isFormValid}
onClick={createProcess}
>
Go

View File

@@ -325,12 +325,18 @@ const ListObjects = ({
if (rewindDate) {
setLoadingRewind(true);
const rewindParsed = rewindDate.toISOString();
let pathPrefix = "";
if (internalPaths) {
const decodedPath = atob(internalPaths);
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/rewind/${rewindParsed}?prefix=${
internalPaths ? `${internalPaths}/` : ""
`/api/v1/buckets/${bucketName}/rewind/${rewindParsed}${
pathPrefix ? `?prefix=${btoa(pathPrefix)}` : ``
}`
)
.then((res: RewindObjectList) => {
@@ -364,17 +370,24 @@ const ListObjects = ({
useEffect(() => {
if (loading) {
let extraPath = "";
let pathPrefix = "";
if (internalPaths) {
extraPath = `?prefix=${internalPaths}/`;
const decodedPath = atob(internalPaths);
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
let currentTimestamp = Date.now() + 0;
let currentTimestamp = Date.now();
setLoadingStartTime(currentTimestamp);
setLoadingMessage(defLoading);
api
.invoke("GET", `/api/v1/buckets/${bucketName}/objects${extraPath}`)
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects${
pathPrefix ? `?prefix=${btoa(pathPrefix)}` : ``
}`
)
.then((res: BucketObjectsList) => {
const records: BucketObject[] = res.objects || [];
const folders: BucketObject[] = [];
@@ -389,19 +402,26 @@ const ListObjects = ({
files.push(record);
}
});
const recordsInElement = [...folders, ...files];
setRecords(recordsInElement);
// In case no objects were retrieved, We check if item is a file
if (!res.objects && extraPath !== "") {
if (!res.objects && pathPrefix !== "") {
if (rewindEnabled) {
const rewindParsed = rewindDate.toISOString();
let pathPrefix = "";
if (internalPaths) {
const decodedPath = atob(internalPaths);
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/rewind/${rewindParsed}?prefix=${
internalPaths ? `${internalPaths}/` : ""
`/api/v1/buckets/${bucketName}/rewind/${rewindParsed}${
pathPrefix ? `?prefix=${btoa(pathPrefix)}` : ``
}`
)
.then((res: RewindObjectList) => {
@@ -426,7 +446,9 @@ const ListObjects = ({
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects?prefix=${internalPaths}`
`/api/v1/buckets/${bucketName}/objects${
internalPaths ? `?prefix=${internalPaths}` : ``
}`
)
.then((res: BucketObjectsList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
@@ -497,7 +519,7 @@ const ListObjects = ({
setCreateFolderOpen(false);
};
const upload = (e: any, bucketName: string, path: string) => {
const upload = (e: any, bucketName: string, encodedPath: string) => {
if (
e === null ||
e === undefined ||
@@ -509,12 +531,11 @@ const ListObjects = ({
e.preventDefault();
let files = e.target.files;
let uploadUrl = `${baseUrl}/api/v1/buckets/${bucketName}/objects/upload`;
if (path !== "") {
const encodedPath = encodeURIComponent(path);
if (encodedPath !== "") {
uploadUrl = `${uploadUrl}?prefix=${encodedPath}`;
}
let xhr = new XMLHttpRequest();
const areMultipleFiles = files.length > 1 ? true : false;
const areMultipleFiles = files.length > 1;
const errorMessage = `An error occurred while uploading the file${
areMultipleFiles ? "s" : ""
}.`;
@@ -602,30 +623,20 @@ const ListObjects = ({
};
const openPath = (idElement: string) => {
const currentPath = get(match, "url", `/buckets/${bucketName}`);
// Element is a folder, we redirect to it
if (idElement.endsWith("/")) {
const idElementClean = idElement
.substr(0, idElement.length - 1)
.split("/");
const lastIndex = idElementClean.length - 1;
const newPath = `${currentPath}/${idElementClean[lastIndex]}`;
history.push(newPath);
return;
}
// Element is a file. we open details here
const pathInArray = idElement.split("/");
const fileName = pathInArray[pathInArray.length - 1];
const newPath = `${currentPath}/${fileName}`;
const newPath = `/buckets/${bucketName}/browse${
idElement ? `/${btoa(idElement)}` : ``
}`;
history.push(newPath);
return;
};
const uploadObject = (e: any): void => {
upload(e, bucketName, `${internalPaths}/`);
let pathPrefix = "";
if (internalPaths) {
const decodedPath = atob(internalPaths);
pathPrefix = decodedPath.endsWith("/") ? decodedPath : decodedPath + "/";
}
upload(e, bucketName, btoa(pathPrefix));
};
const openPreview = (fileObject: BucketObject) => {
@@ -884,7 +895,10 @@ const ListObjects = ({
const ccPath = internalPaths.split("/").pop();
const pageTitle = ccPath !== "" ? ccPath : "/";
const pageTitle = ccPath !== "" ? atob(ccPath) : "/";
// console.log("pageTitle", pageTitle);
const currentPath = pageTitle.split("/").filter((i: string) => i !== "");
// console.log("currentPath", currentPath);
return (
<React.Fragment>
@@ -948,12 +962,14 @@ const ListObjects = ({
<FolderIcon width={40} />
</Fragment>
}
title={pageTitle}
title={
currentPath.length > 0 ? currentPath[currentPath.length - 1] : "/"
}
subTitle={
<Fragment>
<BrowserBreadcrumbs
bucketName={bucketName}
internalPaths={internalPaths}
internalPaths={pageTitle}
/>
</Fragment>
}

View File

@@ -107,7 +107,7 @@ const RewindEnable = ({
name="status"
checked={rewindEnableButton}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setRewindEnableButton(false);
setRewindEnableButton(e.target.checked);
}}
label={"Current Status"}
indicatorLabels={["Enabled", "Disabled"]}

View File

@@ -247,6 +247,7 @@ const ObjectDetails = ({
const [selectedTag, setSelectedTag] = useState<string[]>(["", ""]);
const [legalholdOpen, setLegalholdOpen] = useState<boolean>(false);
const [actualInfo, setActualInfo] = useState<IFileInfo | null>(null);
const [objectToShare, setObjectToShare] = useState<IFileInfo | null>(null);
const [versions, setVersions] = useState<IFileInfo[]>([]);
const [filterVersion, setFilterVersion] = useState<string>("");
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
@@ -255,17 +256,23 @@ const ObjectDetails = ({
const [selectedTab, setSelectedTab] = useState<number>(0);
const internalPaths = get(match.params, "subpaths", "");
const internalPathsDecoded = atob(internalPaths) || "";
const bucketName = match.params["bucketName"];
const allPathData = internalPaths.split("/");
const currentItem = allPathData.pop();
const allPathData = internalPathsDecoded.split("/");
const currentItem = allPathData.pop() || "";
// calculate object name to display
let objectNameArray: string[] = [];
if (actualInfo) {
objectNameArray = actualInfo.name.split("/");
}
useEffect(() => {
if (loadObjectData) {
const encodedPath = encodeURIComponent(internalPaths);
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects?prefix=${encodedPath}${
`/api/v1/buckets/${bucketName}/objects?prefix=${internalPaths}${
distributedSetup ? "&with_versions=true" : ""
}`
)
@@ -299,11 +306,10 @@ const ObjectDetails = ({
useEffect(() => {
if (metadataLoad) {
const encodedPath = encodeURIComponent(internalPaths);
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects?prefix=${encodedPath}&with_metadata=true`
`/api/v1/buckets/${bucketName}/objects?prefix=${internalPaths}&with_metadata=true`
)
.then((res: FileInfoResponse) => {
const fileData = res.objects[0];
@@ -340,6 +346,7 @@ const ObjectDetails = ({
};
const closeShareModal = () => {
setObjectToShare(null);
setShareFileModalOpen(false);
};
@@ -367,8 +374,11 @@ const ObjectDetails = ({
const tableActions: ItemActions[] = [
{
type: "share",
onClick: shareObject,
sendOnlyId: true,
onClick: (item: any) => {
setObjectToShare(item);
shareObject();
},
sendOnlyId: false,
disableButtonFunction: (item: string) => {
const element = versions.find((elm) => elm.version_id === item);
if (element && element.is_delete_marker) {
@@ -445,7 +455,7 @@ const ObjectDetails = ({
open={shareFileModalOpen}
closeModalAndRefresh={closeShareModal}
bucketName={bucketName}
dataObject={actualInfo}
dataObject={objectToShare || actualInfo}
/>
)}
{retentionModalOpen && actualInfo && (
@@ -479,7 +489,7 @@ const ObjectDetails = ({
<DeleteTagModal
deleteOpen={deleteTagModalOpen}
currentTags={actualInfo.tags}
selectedObject={internalPaths}
selectedObject={actualInfo.name}
versionId={actualInfo.version_id}
bucketName={bucketName}
onCloseAndUpdate={closeDeleteTagModal}
@@ -490,7 +500,7 @@ const ObjectDetails = ({
<SetLegalHoldModal
open={legalholdOpen}
closeModalAndRefresh={closeLegalholdModal}
objectName={internalPaths}
objectName={actualInfo.name}
bucketName={bucketName}
actualInfo={actualInfo}
/>
@@ -512,12 +522,16 @@ const ObjectDetails = ({
<ObjectBrowserIcon width={40} />
</Fragment>
}
title={currentItem}
title={
objectNameArray.length > 0
? objectNameArray[objectNameArray.length - 1]
: actualInfo.name
}
subTitle={
<Fragment>
<BrowserBreadcrumbs
bucketName={bucketName}
internalPaths={internalPaths}
internalPaths={actualInfo.name}
/>
</Fragment>
}
@@ -655,7 +669,7 @@ const ObjectDetails = ({
<td className={classes.capitalizeFirst}>
{actualInfo.retention_mode
? actualInfo.retention_mode.toLowerCase()
: "Undefined"}
: "None"}
<IconButton
color="primary"
aria-label="retention"

View File

@@ -76,7 +76,9 @@ const SetLegalHoldModal = ({
api
.invoke(
"PUT",
`/api/v1/buckets/${bucketName}/objects/legalhold?prefix=${objectName}&version_id=${versionId}`,
`/api/v1/buckets/${bucketName}/objects/legalhold?prefix=${btoa(
objectName
)}&version_id=${versionId}`,
{ status: legalHoldEnabled ? "enabled" : "disabled" }
)
.then(() => {

View File

@@ -119,7 +119,9 @@ const SetRetention = ({
api
.invoke(
"PUT",
`/api/v1/buckets/${bucketName}/objects/retention?prefix=${selectedObject}&version_id=${versionId}`,
`/api/v1/buckets/${bucketName}/objects/retention?prefix=${btoa(
selectedObject
)}&version_id=${versionId}`,
{
expires: expireDate,
mode: type,
@@ -142,7 +144,9 @@ const SetRetention = ({
api
.invoke(
"DELETE",
`/api/v1/buckets/${bucketName}/objects/retention?prefix=${selectedObject}&version_id=${versionId}`
`/api/v1/buckets/${bucketName}/objects/retention?prefix=${btoa(
selectedObject
)}&version_id=${versionId}`
)
.then(() => {
setIsSaving(false);

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, Fragment } from "react";
import get from "lodash/get";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
@@ -34,6 +34,7 @@ import api from "../../../../../../common/api";
import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
import PredefinedList from "../../../../Common/FormComponents/PredefinedList/PredefinedList";
import DaysSelector from "../../../../Common/FormComponents/DaysSelector/DaysSelector";
import { LinearProgress } from "@material-ui/core";
const styles = (theme: Theme) =>
createStyles({
@@ -68,9 +69,11 @@ const ShareFile = ({
setModalErrorSnackMessage,
}: IShareFileProps) => {
const [shareURL, setShareURL] = useState<string>("");
const [isLoadingVersion, setIsLoadingVersion] = useState<boolean>(true);
const [isLoadingFile, setIsLoadingFile] = useState<boolean>(false);
const [selectedDate, setSelectedDate] = useState<string>("");
const [dateValid, setDateValid] = useState<boolean>(true);
const [versionID, setVersionID] = useState<string>("null");
const initialDate = new Date();
@@ -85,7 +88,49 @@ const ShareFile = ({
};
useEffect(() => {
if (dateValid) {
// In case version is undefined, we get the latest version of the object
if (dataObject.version_id === undefined) {
// In case it is not distributed setup, then we default to "null";
if (distributedSetup) {
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects?prefix=${btoa(
dataObject.name
)}${distributedSetup ? "&with_versions=true" : ""}`
)
.then((res: IFileInfo[]) => {
const result = get(res, "objects", []);
const latestVersion = result.find(
(elem: IFileInfo) => elem.is_latest
);
if (latestVersion) {
setVersionID(latestVersion.version_id);
return;
}
// Version couldn't ve retrieved, we default
setVersionID("null");
})
.catch((error: ErrorResponseHandler) => {
setModalErrorSnackMessage(error);
});
setIsLoadingVersion(false);
return;
}
setVersionID("null");
setIsLoadingVersion(false);
return;
}
setVersionID(dataObject.version_id || "null");
setIsLoadingVersion(false);
}, [bucketName, dataObject, distributedSetup, setModalErrorSnackMessage]);
useEffect(() => {
if (dateValid && !isLoadingVersion) {
setIsLoadingFile(true);
setShareURL("");
@@ -95,14 +140,12 @@ const ShareFile = ({
const diffDate = slDate.getTime() - currDate.getTime();
if (diffDate > 0) {
const versID = distributedSetup ? dataObject.version_id : "null";
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects/share?prefix=${
`/api/v1/buckets/${bucketName}/objects/share?prefix=${btoa(
dataObject.name
}&version_id=${versID || "null"}${
)}&version_id=${versionID}${
selectedDate !== "" ? `&expires=${diffDate}ms` : ""
}`
)
@@ -125,6 +168,8 @@ const ShareFile = ({
setShareURL,
setModalErrorSnackMessage,
distributedSetup,
isLoadingVersion,
versionID,
]);
return (
@@ -137,42 +182,51 @@ const ShareFile = ({
}}
>
<Grid container className={classes.modalContent}>
<Grid item xs={12} className={classes.moduleDescription}>
This module generates a temporary URL with integrated access
credentials for sharing objects for up to 7 days.
<br />
The temporary URL expires after the configured time limit.
</Grid>
<Grid item xs={12} className={classes.dateContainer}>
<DaysSelector
initialDate={initialDate}
id="date"
label="Active for"
maxDays={7}
onChange={dateChanged}
entity="Link"
/>
</Grid>
<Grid container item xs={12}>
<Grid item xs={10}>
<PredefinedList content={shareURL} />
{isLoadingVersion && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
<Grid item xs={2} className={classes.copyButtonContainer}>
<CopyToClipboard text={shareURL}>
<Button
variant="contained"
color="primary"
startIcon={<CopyIcon />}
onClick={() => {
setModalSnackMessage("Share URL Copied to clipboard");
}}
disabled={shareURL === "" || isLoadingFile}
>
Copy
</Button>
</CopyToClipboard>
</Grid>
</Grid>
)}
{!isLoadingVersion && (
<Fragment>
<Grid item xs={12} className={classes.moduleDescription}>
This module generates a temporary URL with integrated access
credentials for sharing objects for up to 7 days.
<br />
The temporary URL expires after the configured time limit.
</Grid>
<Grid item xs={12} className={classes.dateContainer}>
<DaysSelector
initialDate={initialDate}
id="date"
label="Active for"
maxDays={7}
onChange={dateChanged}
entity="Link"
/>
</Grid>
<Grid container item xs={12}>
<Grid item xs={10}>
<PredefinedList content={shareURL} />
</Grid>
<Grid item xs={2} className={classes.copyButtonContainer}>
<CopyToClipboard text={shareURL}>
<Button
variant="contained"
color="primary"
startIcon={<CopyIcon />}
onClick={() => {
setModalSnackMessage("Share URL Copied to clipboard");
}}
disabled={shareURL === "" || isLoadingFile}
>
Copy
</Button>
</CopyToClipboard>
</Grid>
</Grid>
</Fragment>
)}
</Grid>
</ModalWrapper>
</React.Fragment>

View File

@@ -72,7 +72,7 @@ const PreviewFile = ({
let path = "";
if (object) {
const encodedPath = encodeURIComponent(object.name);
const encodedPath = btoa(object.name);
path = `${window.location.origin}/api/v1/buckets/${bucketName}/objects/download?preview=true&prefix=${encodedPath}`;
if (object.version_id) {
path = path.concat(`&version_id=${object.version_id}`);

View File

@@ -14,8 +14,6 @@
// 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 { isNullOrUndefined } from "util";
export const download = (
bucketName: string,
objectPath: string,
@@ -25,9 +23,9 @@ export const download = (
) => {
const anchor = document.createElement("a");
document.body.appendChild(anchor);
const encodedPath = encodeURIComponent(objectPath);
const encodedPath = btoa(objectPath);
let path = `/api/v1/buckets/${bucketName}/objects/download?prefix=${encodedPath}`;
if (!isNullOrUndefined(versionID) && versionID !== "null") {
if (versionID) {
path = path.concat(`&version_id=${versionID}`);
}
window.location.href = path;

View File

@@ -0,0 +1,245 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useState, useEffect, useCallback } from "react";
import { connect } from "react-redux";
import get from "lodash/get";
import ArrowRightIcon from "@material-ui/icons/ArrowRight";
import ErrorOutlineIcon from "@material-ui/icons/ErrorOutline";
import CloseIcon from "@material-ui/icons/Close";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { AppState } from "../../../../../store";
import { setErrorSnackMessage } from "../../../../../actions";
import { snackBarMessage } from "../../../../../types";
import { setModalErrorSnackMessage } from "../../../../../actions";
interface ImodalErrorProps {
customStyle?: any;
classes: any;
modalSnackMessage: snackBarMessage;
displayErrorMessage: typeof setErrorSnackMessage;
}
const styles = (theme: Theme) =>
createStyles({
modalErrorContainer: {
position: "absolute",
marginTop: 10,
width: "80%",
backgroundColor: "#fff",
border: "#C72C48 1px solid",
borderLeftWidth: 12,
borderRadius: 3,
zIndex: 1000,
padding: "10px 15px",
left: "50%",
transform: "translateX(-50%)",
opacity: 0,
transitionDuration: "0.2s",
},
modalErrorShow: {
opacity: 1,
},
closeButton: {
position: "absolute",
right: 5,
fontSize: "small",
border: 0,
backgroundColor: "#fff",
cursor: "pointer",
},
errorTitle: {
display: "flex",
alignItems: "center",
},
errorLabel: {
color: "#000",
fontSize: 18,
fontWeight: 500,
marginLeft: 5,
marginRight: 25,
},
messageIcon: {
color: "#C72C48",
display: "flex",
"& svg": {
width: 32,
height: 32,
},
},
simpleError: {
marginTop: 5,
padding: "2px 5px",
fontSize: 16,
color: "#000",
},
detailsButton: {
color: "#9C9C9C",
display: "flex",
alignItems: "center",
border: 0,
backgroundColor: "transparent",
paddingLeft: 5,
fontSize: 14,
transformDuration: "0.3s",
cursor: "pointer",
},
extraDetailsContainer: {
fontStyle: "italic",
color: "#9C9C9C",
lineHeight: 0,
padding: "0 10px",
transition: "all .2s ease-in-out",
overflow: "hidden",
},
extraDetailsOpen: {
lineHeight: 1,
padding: "3px 10px",
},
arrowElement: {
marginLeft: -5,
},
arrowOpen: {
transform: "rotateZ(90deg)",
transformDuration: "0.3s",
},
});
var timerI: any;
const startHideTimer = (callbackFunction: () => void) => {
timerI = setInterval(callbackFunction, 10000);
};
const stopHideTimer = () => {
clearInterval(timerI);
};
const ModalError = ({
classes,
modalSnackMessage,
displayErrorMessage,
customStyle,
}: ImodalErrorProps) => {
const [detailsOpen, setDetailsOpen] = useState<boolean>(false);
const [displayErrorMsg, setDisplayErrorMsg] = useState<boolean>(false);
const closeErrorMessage = useCallback(() => {
setDisplayErrorMsg(false);
}, []);
useEffect(() => {
if (!displayErrorMsg) {
displayErrorMessage({ detailedError: "", errorMessage: "" });
setDetailsOpen(false);
//clearInterval(timerI);
}
}, [displayErrorMessage, displayErrorMsg]);
useEffect(() => {
if (
modalSnackMessage.message !== "" &&
modalSnackMessage.type === "error"
) {
//Error message received, we trigger the animation
setDisplayErrorMsg(true);
//startHideTimer(closeErrorMessage);
}
}, [closeErrorMessage, modalSnackMessage.message, modalSnackMessage.type]);
const detailsToggle = () => {
setDetailsOpen(!detailsOpen);
};
const message = get(modalSnackMessage, "message", "");
const messageDetails = get(modalSnackMessage, "detailedErrorMsg", "");
if (modalSnackMessage.type !== "error" || message === "") {
return null;
}
return (
<Fragment>
<div
className={`${classes.modalErrorContainer} ${
displayErrorMsg ? classes.modalErrorShow : ""
}`}
style={customStyle}
onMouseOver={stopHideTimer}
onMouseLeave={() => startHideTimer(closeErrorMessage)}
>
<button className={classes.closeButton} onClick={closeErrorMessage}>
<CloseIcon />
</button>
<div className={classes.errorTitle}>
<span className={classes.messageIcon}>
<ErrorOutlineIcon />
</span>
<span className={classes.errorLabel}>{message}</span>
</div>
{messageDetails !== "" && (
<Fragment>
<div className={classes.detailsContainerLink}>
<button className={classes.detailsButton} onClick={detailsToggle}>
Details
<ArrowRightIcon
className={`${classes.arrowElement} ${
detailsOpen ? classes.arrowOpen : ""
}`}
/>
</button>
</div>
<div
className={`${classes.extraDetailsContainer} ${
detailsOpen ? classes.extraDetailsOpen : ""
}`}
>
{messageDetails}
</div>
</Fragment>
)}
</div>
</Fragment>
);
};
const mapState = (state: AppState) => ({
modalSnackMessage: state.system.modalSnackBar,
});
const mapDispatchToProps = {
displayErrorMessage: setModalErrorSnackMessage,
};
const connector = connect(mapState, mapDispatchToProps);
export default connector(withStyles(styles)(ModalError));

View File

@@ -132,6 +132,7 @@ export const radioIcons = {
export const containerForHeader = (bottomSpacing: any) => ({
container: {
position: "relative" as const,
padding: "8px 16px 0",
"& h6": {
color: "#777777",
@@ -417,6 +418,7 @@ export const widgetCommon = {
minWidth: 280,
maxWidth: 1185,
border: "#eef1f4 2px solid",
backgroundColor: "#fff",
borderRadius: 10,
width: "100%",
padding: 16,
@@ -429,6 +431,8 @@ export const widgetCommon = {
borderBottom: "#eef1f4 1px solid",
paddingBottom: 14,
marginBottom: 5,
display: "flex" as const,
justifyContent: "space-between" as const,
},
contentContainer: {
justifyContent: "center" as const,
@@ -468,6 +472,39 @@ export const widgetCommon = {
overflow: "hidden" as const,
textOverflow: "ellipsis" as const,
},
zoomChartCont: {
position: "relative" as const,
height: 340,
width: "100%",
},
zoomChartIcon: {
backgroundColor: "transparent",
border: 0,
padding: 0,
cursor: "pointer",
"& svg": {
color: "#D0D0D0",
height: 16,
},
"&:hover": {
"& svg": {
color: "#404143",
},
},
},
};
export const widgetContainerCommon = {
widgetPanelDelimiter: {
padding: 10,
},
dashboardRow: {
display: "flex" as const,
flexDirection: "row" as const,
justifyContent: "flex-start" as const,
flexWrap: "wrap" as const,
maxWidth: 1180,
},
};
export const tooltipCommon = {

View File

@@ -23,6 +23,7 @@ import { snackBarCommon } from "../FormComponents/common/styleLibrary";
import { AppState } from "../../../../store";
import { snackBarMessage } from "../../../../types";
import { setModalSnackMessage } from "../../../../actions";
import ModalError from "../FormComponents/ModalError/ModalError";
interface IModalProps {
classes: any;
@@ -124,6 +125,10 @@ const ModalWrapper = ({
}: IModalProps) => {
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
useEffect(() => {
setModalSnackMessage("");
}, [setModalSnackMessage]);
useEffect(() => {
if (modalSnackMessage) {
if (modalSnackMessage.message === "") {
@@ -131,7 +136,9 @@ const ModalWrapper = ({
return;
}
// Open SnackBar
setOpenSnackbar(true);
if (modalSnackMessage.type !== "error") {
setOpenSnackbar(true);
}
}
}, [modalSnackMessage]);
@@ -169,6 +176,7 @@ const ModalWrapper = ({
{...customSize}
>
<div className={classes.dialogContainer}>
<ModalError />
<Snackbar
open={openSnackbar}
className={classes.snackBarModal}

View File

@@ -25,6 +25,7 @@ import DriveInfoCard from "./DriveInfoCard";
import CommonCard from "../CommonCard";
import TabSelector from "../../Common/TabSelector/TabSelector";
import GeneralUsePaginator from "../../Common/GeneralUsePaginator/GeneralUsePaginator";
import { widgetContainerCommon } from "../../Common/FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
@@ -48,6 +49,7 @@ const styles = (theme: Theme) =>
maxWidth: 1185,
width: "100%",
},
...widgetContainerCommon,
});
interface IDashboardProps {
@@ -93,10 +95,12 @@ const BasicDashboard = ({ classes, usage }: IDashboardProps) => {
}
return 0;
});
} else return [];
}
return [];
};
const serverArray = makeServerArray(usage);
const serverArray = makeServerArray(usage || null);
const usageToRepresent = prettyUsage(
usage && usage.usage ? usage.usage.toString() : "0"
@@ -129,29 +133,65 @@ const BasicDashboard = ({ classes, usage }: IDashboardProps) => {
<Grid item xs={12} className={classes.generalStatusTitle}>
General Status
</Grid>
<Grid item xs={12} className={classes.generalStatusCards}>
<CommonCard
title={"All Buckets"}
metricValue={usage ? prettyNumber(usage.buckets) : 0}
extraMargin
/>
<CommonCard
title={"Usage"}
metricValue={usageToRepresent.total}
metricUnit={usageToRepresent.unit}
extraMargin
/>
<CommonCard
title={"Total Objects"}
metricValue={usage ? prettyNumber(usage.objects) : 0}
extraMargin
/>
<CommonCard
title={"Servers"}
metricValue={usage ? prettyNumber(serverArray.length) : 0}
subMessage={{ message: "Total" }}
extraMargin
/>
<Grid item xs={12} className={classes.dashboardRow}>
<Grid
item
xs={7}
sm={8}
md={6}
lg={3}
className={classes.widgetPanelDelimiter}
>
<CommonCard
title={"All Buckets"}
metricValue={usage ? prettyNumber(usage.buckets) : 0}
extraMargin
/>
</Grid>
<Grid
item
xs={7}
sm={8}
md={6}
lg={3}
className={classes.widgetPanelDelimiter}
>
<CommonCard
title={"Usage"}
metricValue={usageToRepresent.total}
metricUnit={usageToRepresent.unit}
extraMargin
/>
</Grid>
<Grid
item
xs={7}
sm={8}
md={6}
lg={3}
className={classes.widgetPanelDelimiter}
>
<CommonCard
title={"Total Objects"}
metricValue={usage ? prettyNumber(usage.objects) : 0}
extraMargin
/>
</Grid>
<Grid
item
xs={7}
sm={8}
md={6}
lg={3}
className={classes.widgetPanelDelimiter}
>
<CommonCard
title={"Servers"}
metricValue={usage ? prettyNumber(serverArray.length) : 0}
subMessage={{ message: "Total" }}
extraMargin
/>
</Grid>
</Grid>
<Grid item xs={12}>
<TabSelector

View File

@@ -32,7 +32,7 @@ export default function Chart() {
return (
<React.Fragment>
<Title>Today</Title>
<ResponsiveContainer>
<ResponsiveContainer width="99%">
<LineChart
data={data}
margin={{

View File

@@ -46,7 +46,9 @@ const styles = (theme: Theme) =>
...widgetCommon,
cardRoot: {
...widgetCommon.singleValueContainer,
maxWidth: 280,
"&.MuiPaper-root": {
borderRadius: 10,
},
},
cardsContainer: {
maxHeight: 440,

View File

@@ -20,41 +20,37 @@ import Grid from "@material-ui/core/Grid";
import ScheduleIcon from "@material-ui/icons/Schedule";
import WatchLaterIcon from "@material-ui/icons/WatchLater";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button } from "@material-ui/core";
import { actionsTray } from "../../Common/FormComponents/common/styleLibrary";
import { IDashboardPanel, widgetType } from "./types";
import { Button, GridSize } from "@material-ui/core";
import {
actionsTray,
widgetContainerCommon,
} from "../../Common/FormComponents/common/styleLibrary";
import { IDashboardPanel } from "./types";
import { getWidgetsWithValue, panelsConfiguration } from "./utils";
import { TabPanel } from "../../../shared/tabs";
import { ErrorResponseHandler } from "../../../../common/types";
import { setErrorSnackMessage } from "../../../../actions";
import SingleValueWidget from "./Widgets/SingleValueWidget";
import LinearGraphWidget from "./Widgets/LinearGraphWidget";
import BarChartWidget from "./Widgets/BarChartWidget";
import PieChartWidget from "./Widgets/PieChartWidget";
import SingleRepWidget from "./Widgets/SingleRepWidget";
import DateTimePickerWrapper from "../../Common/FormComponents/DateTimePickerWrapper/DateTimePickerWrapper";
import api from "../../../../common/api";
import SyncIcon from "../../../../icons/SyncIcon";
import TabSelector from "../../Common/TabSelector/TabSelector";
import SimpleWidget from "./Widgets/SimpleWidget";
import MergedWidgets from "./MergedWidgets";
import { componentToUse } from "./widgetUtils";
import ZoomWidget from "./ZoomWidget";
import { AppState } from "../../../../store";
interface IPrDashboard {
classes: any;
displayErrorMessage: typeof setErrorSnackMessage;
apiPrefix?: string;
zoomOpen: boolean;
zoomWidget: null | IDashboardPanel;
}
const styles = (theme: Theme) =>
createStyles({
...actionsTray,
widgetsContainer: {
position: "relative",
display: "flex",
flexGrow: 1,
width: "100%",
height: "100%",
},
...widgetContainerCommon,
syncButton: {
"&.MuiButton-root .MuiButton-iconSizeMedium > *:first-child": {
fontSize: 18,
@@ -71,9 +67,6 @@ const styles = (theme: Theme) =>
flexWrap: "wrap",
maxWidth: 1180,
},
widgetPanelDelimiter: {
margin: 10,
},
schedulerIcon: {
opacity: 0.4,
fontSize: 10,
@@ -88,6 +81,8 @@ const PrDashboard = ({
classes,
displayErrorMessage,
apiPrefix = "admin",
zoomOpen,
zoomWidget,
}: IPrDashboard) => {
const [timeStart, setTimeStart] = useState<any>(null);
const [timeEnd, setTimeEnd] = useState<any>(null);
@@ -98,92 +93,16 @@ const PrDashboard = ({
const panels = useCallback(
(tabName: string, filterPanels?: number[][] | null) => {
const componentToUse = (value: IDashboardPanel, index: number) => {
switch (value.type) {
case widgetType.singleValue:
return (
<SingleValueWidget
title={value.title}
panelItem={value}
timeStart={timeStart}
timeEnd={timeEnd}
propLoading={loading}
apiPrefix={apiPrefix}
/>
);
case widgetType.simpleWidget:
return (
<SimpleWidget
title={value.title}
panelItem={value}
timeStart={timeStart}
timeEnd={timeEnd}
propLoading={loading}
apiPrefix={apiPrefix}
iconWidget={value.widgetIcon}
/>
);
case widgetType.pieChart:
return (
<PieChartWidget
title={value.title}
panelItem={value}
timeStart={timeStart}
timeEnd={timeEnd}
propLoading={loading}
apiPrefix={apiPrefix}
/>
);
case widgetType.linearGraph:
case widgetType.areaGraph:
return (
<LinearGraphWidget
title={value.title}
panelItem={value}
timeStart={timeStart}
timeEnd={timeEnd}
propLoading={loading}
hideYAxis={value.disableYAxis}
xAxisFormatter={value.xAxisFormatter}
yAxisFormatter={value.yAxisFormatter}
apiPrefix={apiPrefix}
areaWidget={value.type === widgetType.areaGraph}
/>
);
case widgetType.barChart:
return (
<BarChartWidget
title={value.title}
panelItem={value}
timeStart={timeStart}
timeEnd={timeEnd}
propLoading={loading}
apiPrefix={apiPrefix}
/>
);
case widgetType.singleRep:
const fillColor = value.fillColor ? value.fillColor : value.color;
return (
<SingleRepWidget
title={value.title}
panelItem={value}
timeStart={timeStart}
timeEnd={timeEnd}
propLoading={loading}
color={value.color as string}
fillColor={fillColor as string}
apiPrefix={apiPrefix}
/>
);
default:
return null;
}
};
return filterPanels?.map((panelLine, indexLine) => {
const totalPanelsContained = panelLine.length;
const perc = 100 / totalPanelsContained;
let perc = Math.floor(12 / totalPanelsContained);
if (perc < 1) {
perc = 1;
} else if (perc > 12) {
perc = 12;
}
return (
<Grid
@@ -198,33 +117,51 @@ const PrDashboard = ({
);
return (
<div
<Grid
key={`widget-${panelInline}-${indexPanel}`}
className={classes.widgetPanelDelimiter}
style={{ width: `calc(${perc}% - 20px)` }}
item
xs={7}
sm={8}
md={6}
lg={perc as GridSize}
>
{panelInfo ? (
<Fragment>
{panelInfo.mergedPanels ? (
<Fragment>
<MergedWidgets
title={panelInfo.title}
leftComponent={componentToUse(
panelInfo.mergedPanels[0],
0
)}
rightComponent={componentToUse(
panelInfo.mergedPanels[1],
1
)}
/>
</Fragment>
) : (
componentToUse(panelInfo, indexPanel)
)}
</Fragment>
) : null}
</div>
<Grid item xs={12}>
{panelInfo ? (
<Fragment>
{panelInfo.mergedPanels ? (
<Fragment>
<MergedWidgets
title={panelInfo.title}
leftComponent={componentToUse(
panelInfo.mergedPanels[0],
timeStart,
timeEnd,
loading,
apiPrefix
)}
rightComponent={componentToUse(
panelInfo.mergedPanels[1],
timeStart,
timeEnd,
loading,
apiPrefix
)}
/>
</Fragment>
) : (
componentToUse(
panelInfo,
timeStart,
timeEnd,
loading,
apiPrefix
)
)}
</Fragment>
) : null}
</Grid>
</Grid>
);
})}
</Grid>
@@ -292,11 +229,11 @@ const PrDashboard = ({
}, [loading, fetchUsage]);
const summaryPanels = [
[66, 44, 500, 501],
[50, 502],
[80, 81, 1],
[68, 52],
[63, 70],
[66, 50, 44, 500],
[501, 502, 61, 62],
];
const resourcesPanels = [
[76, 77],
@@ -307,6 +244,16 @@ const PrDashboard = ({
return (
<Fragment>
{zoomOpen && (
<ZoomWidget
modalOpen={zoomOpen}
timeStart={timeStart}
timeEnd={timeEnd}
widgetRender={0}
value={zoomWidget}
apiPrefix={apiPrefix}
/>
)}
<Grid
item
xs={12}
@@ -353,7 +300,7 @@ const PrDashboard = ({
setCurTab(newValue);
}}
tabOptions={[
{ label: "Summary" },
{ label: "Usage" },
{ label: "Traffic" },
{ label: "Resources" },
]}
@@ -373,11 +320,13 @@ const PrDashboard = ({
</Fragment>
);
};
/*
<
*/
const connector = connect(null, {
const mapState = (state: AppState) => ({
zoomOpen: state.dashboard.zoom.openZoom,
zoomWidget: state.dashboard.zoom.widgetRender,
});
const connector = connect(mapState, {
displayErrorMessage: setErrorSnackMessage,
});

View File

@@ -28,6 +28,7 @@ import {
import { MaterialUiPickersDate } from "@material-ui/pickers/typings/date";
import { CircularProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import ZoomOutMapIcon from "@material-ui/icons/ZoomOutMap";
import { IBarChartConfiguration } from "./types";
import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary";
import BarChartTooltip from "./tooltips/BarChartTooltip";
@@ -36,6 +37,7 @@ import { IDashboardPanel } from "../types";
import { widgetDetailsToPanel } from "../utils";
import { ErrorResponseHandler } from "../../../../../common/types";
import api from "../../../../../common/api";
import { openZoomPage } from "../../actions";
interface IBarChartWidget {
classes: any;
@@ -46,6 +48,8 @@ interface IBarChartWidget {
propLoading: boolean;
displayErrorMessage: any;
apiPrefix: string;
zoomActivated?: boolean;
openZoomPage: typeof openZoomPage;
}
const styles = (theme: Theme) =>
@@ -84,6 +88,8 @@ const BarChartWidget = ({
propLoading,
displayErrorMessage,
apiPrefix,
zoomActivated = false,
openZoomPage,
}: IBarChartWidget) => {
const [loading, setLoading] = useState<boolean>(true);
const [data, setData] = useState<any>([]);
@@ -147,16 +153,32 @@ const BarChartWidget = ({
}
return (
<div className={classes.singleValueContainer}>
<div className={classes.titleContainer}>{title}</div>
<div className={zoomActivated ? "" : classes.singleValueContainer}>
{!zoomActivated && (
<div className={classes.titleContainer}>
{title}{" "}
<button
onClick={() => {
openZoomPage(panelItem);
}}
className={classes.zoomChartIcon}
>
<ZoomOutMapIcon />
</button>
</div>
)}
{loading && (
<div className={classes.loadingAlign}>
<CircularProgress />
</div>
)}
{!loading && (
<div className={classes.contentContainer}>
<ResponsiveContainer>
<div
className={
zoomActivated ? classes.zoomChartCont : classes.contentContainer
}
>
<ResponsiveContainer width="99%">
<BarChart
data={data as object[]}
layout={"vertical"}
@@ -178,7 +200,7 @@ const BarChartWidget = ({
dataKey={bar.dataKey}
fill={bar.color}
background={bar.background}
barSize={12}
barSize={zoomActivated ? 25 : 12}
>
{barChartConfiguration.length === 1 ? (
<Fragment>
@@ -214,6 +236,7 @@ const BarChartWidget = ({
const connector = connect(null, {
displayErrorMessage: setErrorSnackMessage,
openZoomPage: openZoomPage,
});
export default withStyles(styles)(connector(BarChartWidget));

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import React, { Fragment, useEffect, useState } from "react";
import { connect } from "react-redux";
import {
Area,
@@ -28,6 +28,7 @@ import {
import { CircularProgress } from "@material-ui/core";
import { MaterialUiPickersDate } from "@material-ui/pickers/typings/date";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import ZoomOutMapIcon from "@material-ui/icons/ZoomOutMap";
import { ILinearGraphConfiguration } from "./types";
import { widgetCommon } from "../../../Common/FormComponents/common/styleLibrary";
import { IDashboardPanel } from "../types";
@@ -36,6 +37,7 @@ import { widgetDetailsToPanel } from "../utils";
import { ErrorResponseHandler } from "../../../../../common/types";
import api from "../../../../../common/api";
import LineChartTooltip from "./tooltips/LineChartTooltip";
import { openZoomPage } from "../../actions";
interface ILinearGraphWidget {
classes: any;
@@ -50,6 +52,8 @@ interface ILinearGraphWidget {
yAxisFormatter?: (item: string) => string;
xAxisFormatter?: (item: string) => string;
areaWidget?: boolean;
zoomActivated?: boolean;
openZoomPage: typeof openZoomPage;
}
const styles = (theme: Theme) =>
@@ -61,6 +65,9 @@ const styles = (theme: Theme) =>
height: "100%",
flexGrow: 1,
},
verticalAlignment: {
flexDirection: "column",
},
chartCont: {
position: "relative",
height: 140,
@@ -70,7 +77,7 @@ const styles = (theme: Theme) =>
display: "flex",
flexDirection: "column",
flex: "0 1 auto",
height: 130,
maxHeight: 130,
margin: 0,
overflowY: "auto",
position: "relative",
@@ -99,6 +106,8 @@ const LinearGraphWidget = ({
areaWidget = false,
yAxisFormatter = (item: string) => item,
xAxisFormatter = (item: string) => item,
zoomActivated = false,
openZoomPage,
}: ILinearGraphWidget) => {
const [loading, setLoading] = useState<boolean>(true);
const [data, setData] = useState<object[]>([]);
@@ -174,14 +183,34 @@ const LinearGraphWidget = ({
};
return (
<div className={classes.singleValueContainer}>
<div className={classes.titleContainer}>{title}</div>
<div className={classes.containerElements}>
<div className={zoomActivated ? "" : classes.singleValueContainer}>
{!zoomActivated && (
<div className={classes.titleContainer}>
{title}{" "}
<button
onClick={() => {
openZoomPage(panelItem);
}}
className={classes.zoomChartIcon}
>
<ZoomOutMapIcon />
</button>
</div>
)}
<div
className={
zoomActivated ? classes.verticalAlignment : classes.containerElements
}
>
{loading && <CircularProgress className={classes.loadingAlign} />}
{!loading && (
<React.Fragment>
<div className={classes.chartCont}>
<ResponsiveContainer>
<div
className={
zoomActivated ? classes.zoomChartCont : classes.chartCont
}
>
<ResponsiveContainer width="99%">
<AreaChart
data={data}
margin={{
@@ -267,24 +296,33 @@ const LinearGraphWidget = ({
</ResponsiveContainer>
</div>
{!areaWidget && (
<div className={classes.legendChart}>
{linearConfiguration.map((section, index) => {
return (
<div
className={classes.singleLegendContainer}
key={`legend-${section.keyLabel}-${index.toString()}`}
>
<Fragment>
{zoomActivated && (
<Fragment>
<strong>Series</strong>
<br />
<br />
</Fragment>
)}
<div className={classes.legendChart}>
{linearConfiguration.map((section, index) => {
return (
<div
className={classes.colorContainer}
style={{ backgroundColor: section.lineColor }}
/>
<div className={classes.legendLabel}>
{section.keyLabel}
className={classes.singleLegendContainer}
key={`legend-${section.keyLabel}-${index.toString()}`}
>
<div
className={classes.colorContainer}
style={{ backgroundColor: section.lineColor }}
/>
<div className={classes.legendLabel}>
{section.keyLabel}
</div>
</div>
</div>
);
})}
</div>
);
})}
</div>
</Fragment>
)}
</React.Fragment>
)}
@@ -295,6 +333,7 @@ const LinearGraphWidget = ({
const connector = connect(null, {
displayErrorMessage: setErrorSnackMessage,
openZoomPage: openZoomPage,
});
export default withStyles(styles)(connector(LinearGraphWidget));

View File

@@ -143,7 +143,7 @@ const PieChartWidget = ({
{middleLabel && splitSizeMetric(middleLabel)}
</span>
<div className={classes.chartContainer}>
<ResponsiveContainer>
<ResponsiveContainer width="99%">
<PieChart margin={{ top: 5, bottom: 5 }}>
{dataOuter && (
<Pie

View File

@@ -27,6 +27,10 @@ import { widgetDetailsToPanel } from "../utils";
import { CircularProgress } from "@material-ui/core";
import { ErrorResponseHandler } from "../../../../../common/types";
import api from "../../../../../common/api";
import {
prettyNumber,
representationNumber,
} from "../../../../../common/utils";
interface ISingleRepWidget {
classes: any;
@@ -119,7 +123,7 @@ const SingleRepWidget = ({
)}
{!loading && (
<div className={classes.contentContainer}>
<ResponsiveContainer>
<ResponsiveContainer width="99%">
<AreaChart data={data}>
<defs>
<linearGradient id={gradientID} x1="0" y1="0" x2="0" y2="1">
@@ -144,10 +148,12 @@ const SingleRepWidget = ({
textAnchor="start"
dominantBaseline="auto"
fontWeight={700}
fontSize={70}
fontSize={65}
fill={"#07193E"}
>
{result ? result.innerLabel : ""}
{result
? representationNumber(parseInt(result.innerLabel || "0"))
: ""}
</text>
</AreaChart>
</ResponsiveContainer>

View File

@@ -0,0 +1,66 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment } from "react";
import { connect } from "react-redux";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import { IDashboardPanel } from "./types";
import { componentToUse } from "./widgetUtils";
import { closeZoomPage } from "../actions";
interface IZoomWidget {
widgetRender: number;
value: IDashboardPanel | null;
modalOpen: boolean;
timeStart: any;
timeEnd: any;
apiPrefix: string;
onCloseAction: typeof closeZoomPage;
}
const ZoomWidget = ({
value,
modalOpen,
timeStart,
timeEnd,
apiPrefix,
onCloseAction,
}: IZoomWidget) => {
if (!value) {
return null;
}
return (
<ModalWrapper
title={value.title}
onClose={() => {
onCloseAction();
}}
modalOpen={modalOpen}
wideLimit={false}
noContentPadding
>
<Fragment>
{componentToUse(value, timeStart, timeEnd, true, apiPrefix, true)}
</Fragment>
</ModalWrapper>
);
};
const connector = connect(null, {
onCloseAction: closeZoomPage,
});
export default connector(ZoomWidget);

View File

@@ -21,12 +21,13 @@ import {
getTimeFromTimestamp,
niceBytes,
niceDays,
representationNumber,
textToRGBColor,
units,
} from "../../../../common/utils";
import HealIcon from "../../../../icons/HealIcon";
import DiagnosticsIcon from "../../../../icons/DiagnosticsIcon";
import HistoryIcon from "../../../../icons/HistoryIcon";
import { UptimeIcon } from "../../../../icons";
const colorsMain = [
"#C4D4E9",
@@ -54,12 +55,12 @@ export const panelsConfiguration: IDashboardPanel[] = [
title: "Uptime",
data: "N/A",
type: widgetType.simpleWidget,
widgetIcon: <HistoryIcon />,
widgetIcon: <UptimeIcon />,
labelDisplayFunction: niceDays,
},
{
id: 50,
title: "Current Usable Capacity",
title: "Capacity",
data: [],
dataOuter: [{ name: "outer", value: 100 }],
widgetConfiguration: {
@@ -143,7 +144,7 @@ export const panelsConfiguration: IDashboardPanel[] = [
},
{
id: 66,
title: "Number of Buckets",
title: "Buckets",
data: [],
innerLabel: "N/A",
type: widgetType.singleRep,
@@ -152,7 +153,7 @@ export const panelsConfiguration: IDashboardPanel[] = [
},
{
id: 44,
title: "Number of Objects",
title: "Objects",
data: [],
innerLabel: "N/A",
type: widgetType.singleRep,
@@ -161,7 +162,7 @@ export const panelsConfiguration: IDashboardPanel[] = [
},
{
id: 63,
title: "S3 API Data Received Rate",
title: "API Data Received Rate",
data: [],
widgetConfiguration: [
{
@@ -213,7 +214,7 @@ export const panelsConfiguration: IDashboardPanel[] = [
},
{
id: 60,
title: "S3 API Request Rate",
title: "API Request Rate",
data: [],
widgetConfiguration: [
{
@@ -229,7 +230,7 @@ export const panelsConfiguration: IDashboardPanel[] = [
},
{
id: 70,
title: "S3 API Data Sent Rate",
title: "API Data Sent Rate",
data: [],
widgetConfiguration: [
{
@@ -296,7 +297,7 @@ export const panelsConfiguration: IDashboardPanel[] = [
},
{
id: 71,
title: "S3 API Request Error Rate",
title: "API Request Error Rate",
data: [],
widgetConfiguration: [
{
@@ -398,13 +399,13 @@ export const panelsConfiguration: IDashboardPanel[] = [
mergedPanels: [
{
id: 53,
title: "Online Servers",
title: "Online",
data: "N/A",
type: widgetType.singleValue,
},
{
id: 69,
title: "Offline Servers",
title: "Offline",
data: "N/A",
type: widgetType.singleValue,
},
@@ -416,25 +417,25 @@ export const panelsConfiguration: IDashboardPanel[] = [
mergedPanels: [
{
id: 9,
title: "Online Disks",
title: "Online",
data: "N/A",
type: widgetType.singleValue,
},
{
id: 78,
title: "Offline Disks",
title: "Offline",
data: "N/A",
type: widgetType.singleValue,
},
],
title: "Disks",
title: "Drives",
},
{
id: 502,
mergedPanels: [
{
id: 65,
title: "Inbound Traffic",
title: "Upload",
data: "N/A",
type: widgetType.singleValue,
@@ -442,14 +443,14 @@ export const panelsConfiguration: IDashboardPanel[] = [
},
{
id: 64,
title: "Outbound Traffic",
title: "Download",
data: "N/A",
type: widgetType.singleValue,
labelDisplayFunction: niceBytes,
},
],
title: "Total S3 Traffic",
title: "Network",
},
];
@@ -791,15 +792,29 @@ export const widgetDetailsToPanel = (
return panelItem;
};
const verifyNumeric = (item: string) => {
return !isNaN(parseFloat(item));
};
export const splitSizeMetric = (val: string) => {
const splittedText = val.split(" ");
// Value is not a size metric, we return as common string
const singleValue = () => {
let vl = val;
if (verifyNumeric(val)) {
vl = representationNumber(parseFloat(val));
}
return <Fragment>{vl}</Fragment>;
};
if (splittedText.length !== 2) {
return <Fragment>{val}</Fragment>;
return singleValue();
}
if (!units.includes(splittedText[1])) {
return <Fragment>{val}</Fragment>;
return singleValue();
}
return (

View File

@@ -0,0 +1,115 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { IDashboardPanel, widgetType } from "./types";
import BarChartWidget from "./Widgets/BarChartWidget";
import LinearGraphWidget from "./Widgets/LinearGraphWidget";
import PieChartWidget from "./Widgets/PieChartWidget";
import SimpleWidget from "./Widgets/SimpleWidget";
import SingleRepWidget from "./Widgets/SingleRepWidget";
import SingleValueWidget from "./Widgets/SingleValueWidget";
export const componentToUse = (
value: IDashboardPanel,
timeStart: any,
timeEnd: any,
loading: boolean,
apiPrefix: string,
zoomActivated: boolean = false
) => {
switch (value.type) {
case widgetType.singleValue:
return (
<SingleValueWidget
title={value.title}
panelItem={value}
timeStart={timeStart}
timeEnd={timeEnd}
propLoading={loading}
apiPrefix={apiPrefix}
/>
);
case widgetType.simpleWidget:
return (
<SimpleWidget
title={value.title}
panelItem={value}
timeStart={timeStart}
timeEnd={timeEnd}
propLoading={loading}
apiPrefix={apiPrefix}
iconWidget={value.widgetIcon}
/>
);
case widgetType.pieChart:
return (
<PieChartWidget
title={value.title}
panelItem={value}
timeStart={timeStart}
timeEnd={timeEnd}
propLoading={loading}
apiPrefix={apiPrefix}
/>
);
case widgetType.linearGraph:
case widgetType.areaGraph:
return (
<LinearGraphWidget
title={value.title}
panelItem={value}
timeStart={timeStart}
timeEnd={timeEnd}
propLoading={loading}
hideYAxis={value.disableYAxis}
xAxisFormatter={value.xAxisFormatter}
yAxisFormatter={value.yAxisFormatter}
apiPrefix={apiPrefix}
areaWidget={value.type === widgetType.areaGraph}
zoomActivated={zoomActivated}
/>
);
case widgetType.barChart:
return (
<BarChartWidget
title={value.title}
panelItem={value}
timeStart={timeStart}
timeEnd={timeEnd}
propLoading={loading}
apiPrefix={apiPrefix}
zoomActivated={zoomActivated}
/>
);
case widgetType.singleRep:
const fillColor = value.fillColor ? value.fillColor : value.color;
return (
<SingleRepWidget
title={value.title}
panelItem={value}
timeStart={timeStart}
timeEnd={timeEnd}
propLoading={loading}
color={value.color as string}
fillColor={fillColor as string}
apiPrefix={apiPrefix}
/>
);
default:
return null;
}
};

View File

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

View File

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

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 { IDashboardPanel } from "./Prometheus/types";
export interface Usage {
usage: number;
buckets: number;
@@ -45,3 +47,8 @@ export interface IDriveInfo {
usedSpace: number;
availableSpace: number;
}
export interface zoomState {
openZoom: boolean;
widgetRender: null | IDashboardPanel;
}

View File

@@ -55,20 +55,16 @@ const BrowserBreadcrumbs = ({
paths = `/${internalPaths}`;
}
const splitPaths = paths.split("/");
const splitPaths = paths.split("/").filter((path) => path !== "");
const listBreadcrumbs = splitPaths.map(
(objectItem: string, index: number) => {
const subSplit = splitPaths.slice(1, index + 1).join("/");
const route = `/buckets/${bucketName}/browse${
objectItem !== "" ? `/${subSplit}` : ""
const subSplit = splitPaths.slice(0, index + 1).join("/");
const route = `/buckets/${bucketName}/browse/${
subSplit ? `${btoa(subSplit)}` : ``
}`;
const label = objectItem === "" ? bucketName : objectItem;
return (
<React.Fragment key={`breadcrumbs-${index.toString()}`}>
<Link to={route}>{label}</Link>
<Link to={route}>{objectItem}</Link>
{index < splitPaths.length - 1 && <span> / </span>}
</React.Fragment>
);
@@ -95,6 +91,10 @@ const BrowserBreadcrumbs = ({
)}
<Grid item xs={12} className={classes.breadcrumbs}>
<React.Fragment>
<Link to={`/buckets/${bucketName}/browse`}>{bucketName}</Link>
{listBreadcrumbs.length > 0 && <span> / </span>}
</React.Fragment>
{listBreadcrumbs}
</Grid>
</React.Fragment>

View File

@@ -26,6 +26,7 @@ import { bucketsReducer } from "./screens/Console/Buckets/reducers";
import { objectBrowserReducer } from "./screens/Console/ObjectBrowser/reducers";
import { tenantsReducer } from "./screens/Console/Tenants/reducer";
import { directCSIReducer } from "./screens/Console/DirectCSI/reducer";
import { dashboardReducer } from "./screens/Console/Dashboard/reducer";
const globalReducer = combineReducers({
system: systemReducer,
@@ -38,6 +39,7 @@ const globalReducer = combineReducers({
healthInfo: healthInfoReducer,
tenants: tenantsReducer,
directCSI: directCSIReducer,
dashboard: dashboardReducer,
});
declare global {

View File

@@ -213,7 +213,14 @@ func getUserAddResponse(session *models.Principal, params admin_api.AddUserParam
// create a minioClient interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
var userExists bool
_, err = adminClient.getUserInfo(ctx, *params.Body.AccessKey)
userExists = err == nil
if userExists {
return nil, prepareError(errNonUniqueAccessKey)
}
user, err := addUser(
ctx,
adminClient,

View File

@@ -33,7 +33,8 @@ var (
errLicenseNotFound = errors.New("license not found")
errAvoidSelfAccountDelete = errors.New("logged in user cannot be deleted by itself")
errAccessDenied = errors.New("access denied")
errOauth2Provider = errors.New("error contacting the external identity provider")
errOauth2Provider = errors.New("unable to contact configured identity provider")
errNonUniqueAccessKey = errors.New("access key already in use")
)
// Tiering errors

View File

@@ -39,8 +39,10 @@ func registerAccountHandlers(api *operations.ConsoleAPI) {
}
// Custom response writer to update the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
cookie := NewSessionCookieForConsole(changePasswordResponse.SessionID)
http.SetCookie(w, &cookie)
cookies := NewSessionCookieForConsole(changePasswordResponse.SessionID)
for _, cookie := range cookies {
http.SetCookie(w, &cookie)
}
user_api.NewLoginCreated().WithPayload(changePasswordResponse).WriteResponse(w, p)
})
})

View File

@@ -18,6 +18,7 @@ package restapi
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
@@ -835,13 +836,15 @@ func getBucketObjectLockingResponse(session *models.Principal, bucketName string
func getBucketRewindResponse(session *models.Principal, params user_api.GetBucketRewindParams) (*models.RewindResponse, *models.Error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
var prefix = ""
if params.Prefix != nil {
prefix = *params.Prefix
encodedPrefix := *params.Prefix
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
return nil, prepareError(err)
}
prefix = string(decodedPrefix)
}
s3Client, err := newS3BucketClient(session, params.BucketName, prefix)
if err != nil {
LogError("error creating S3Client: %v", err)

View File

@@ -53,8 +53,10 @@ func registerLoginHandlers(api *operations.ConsoleAPI) {
}
// Custom response writer to set the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
cookie := NewSessionCookieForConsole(loginResponse.SessionID)
http.SetCookie(w, &cookie)
cookies := NewSessionCookieForConsole(loginResponse.SessionID)
for _, cookie := range cookies {
http.SetCookie(w, &cookie)
}
user_api.NewLoginCreated().WithPayload(loginResponse).WriteResponse(w, p)
})
})
@@ -65,8 +67,10 @@ func registerLoginHandlers(api *operations.ConsoleAPI) {
}
// Custom response writer to set the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
cookie := NewSessionCookieForConsole(loginResponse.SessionID)
http.SetCookie(w, &cookie)
cookies := NewSessionCookieForConsole(loginResponse.SessionID)
for _, cookie := range cookies {
http.SetCookie(w, &cookie)
}
user_api.NewLoginOauth2AuthCreated().WithPayload(loginResponse).WriteResponse(w, p)
})
})
@@ -77,8 +81,10 @@ func registerLoginHandlers(api *operations.ConsoleAPI) {
}
// Custom response writer to set the session cookies
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
cookie := NewSessionCookieForConsole(loginResponse.SessionID)
http.SetCookie(w, &cookie)
cookies := NewSessionCookieForConsole(loginResponse.SessionID)
for _, cookie := range cookies {
http.SetCookie(w, &cookie)
}
user_api.NewLoginOperatorCreated().WithPayload(loginResponse).WriteResponse(w, p)
})
})

View File

@@ -18,6 +18,7 @@ package restapi
import (
"context"
"encoding/base64"
"fmt"
"io"
"log"
@@ -83,8 +84,21 @@ func registerObjectsHandlers(api *operations.ConsoleAPI) {
defer resp.Close()
// indicate it's a download / inline content to the browser, and the size of the object
filename := params.Prefix
var prefixPath string
var filename string
if params.Prefix != "" {
encodedPrefix := params.Prefix
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
log.Println(err)
}
prefixPath = string(decodedPrefix)
}
prefixElements := strings.Split(prefixPath, "/")
if len(prefixElements) > 0 {
filename = prefixElements[len(prefixElements)-1]
}
if isPreview {
rw.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
rw.Header().Set("X-Frame-Options", "SAMEORIGIN")
@@ -172,9 +186,13 @@ func getListObjectsResponse(session *models.Principal, params user_api.ListObjec
var recursive bool
var withVersions bool
var withMetadata bool
if params.Prefix != nil {
prefix = *params.Prefix
encodedPrefix := *params.Prefix
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
return nil, prepareError(err)
}
prefix = string(decodedPrefix)
}
if params.Recursive != nil {
recursive = *params.Recursive
@@ -270,7 +288,16 @@ func listBucketObjects(ctx context.Context, client MinioClient, bucketName strin
func getDownloadObjectResponse(session *models.Principal, params user_api.DownloadObjectParams) (io.ReadCloser, *models.Error) {
ctx := context.Background()
s3Client, err := newS3BucketClient(session, params.BucketName, params.Prefix)
var prefix string
if params.Prefix != "" {
encodedPrefix := params.Prefix
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
return nil, prepareError(err)
}
prefix = string(decodedPrefix)
}
s3Client, err := newS3BucketClient(session, params.BucketName, prefix)
if err != nil {
return nil, prepareError(err)
}
@@ -465,7 +492,12 @@ func getUploadObjectResponse(session *models.Principal, params user_api.PostBuck
func uploadFiles(ctx context.Context, client MinioClient, params user_api.PostBucketsBucketNameObjectsUploadParams) error {
var prefix string
if params.Prefix != nil {
prefix = *params.Prefix
encodedPrefix := *params.Prefix
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
return err
}
prefix = string(decodedPrefix)
}
// parse a request body as multipart/form-data.
@@ -507,7 +539,16 @@ func uploadFiles(ctx context.Context, client MinioClient, params user_api.PostBu
// getShareObjectResponse returns a share object url
func getShareObjectResponse(session *models.Principal, params user_api.ShareObjectParams) (*string, *models.Error) {
ctx := context.Background()
s3Client, err := newS3BucketClient(session, params.BucketName, params.Prefix)
var prefix string
if params.Prefix != "" {
encodedPrefix := params.Prefix
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
return nil, prepareError(err)
}
prefix = string(decodedPrefix)
}
s3Client, err := newS3BucketClient(session, params.BucketName, prefix)
if err != nil {
return nil, prepareError(err)
}
@@ -552,7 +593,16 @@ func getSetObjectLegalHoldResponse(session *models.Principal, params user_api.Pu
// create a minioClient interface implementation
// defining the client to be used
minioClient := minioClient{client: mClient}
err = setObjectLegalHold(ctx, minioClient, params.BucketName, params.Prefix, params.VersionID, *params.Body.Status)
var prefix string
if params.Prefix != "" {
encodedPrefix := params.Prefix
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
return prepareError(err)
}
prefix = string(decodedPrefix)
}
err = setObjectLegalHold(ctx, minioClient, params.BucketName, prefix, params.VersionID, *params.Body.Status)
if err != nil {
return prepareError(err)
}
@@ -579,7 +629,16 @@ func getSetObjectRetentionResponse(session *models.Principal, params user_api.Pu
// create a minioClient interface implementation
// defining the client to be used
minioClient := minioClient{client: mClient}
err = setObjectRetention(ctx, minioClient, params.BucketName, params.VersionID, params.Prefix, params.Body)
var prefix string
if params.Prefix != "" {
encodedPrefix := params.Prefix
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
return prepareError(err)
}
prefix = string(decodedPrefix)
}
err = setObjectRetention(ctx, minioClient, params.BucketName, params.VersionID, prefix, params.Body)
if err != nil {
return prepareError(err)
}
@@ -623,7 +682,16 @@ func deleteObjectRetentionResponse(session *models.Principal, params user_api.De
// create a minioClient interface implementation
// defining the client to be used
minioClient := minioClient{client: mClient}
err = deleteObjectRetention(ctx, minioClient, params.BucketName, params.Prefix, params.VersionID)
var prefix string
if params.Prefix != "" {
encodedPrefix := params.Prefix
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
return prepareError(err)
}
prefix = string(decodedPrefix)
}
err = deleteObjectRetention(ctx, minioClient, params.BucketName, prefix, params.VersionID)
if err != nil {
return prepareError(err)
}
@@ -649,7 +717,16 @@ func getPutObjectTagsResponse(session *models.Principal, params user_api.PutObje
// create a minioClient interface implementation
// defining the client to be used
minioClient := minioClient{client: mClient}
err = putObjectTags(ctx, minioClient, params.BucketName, params.Prefix, params.VersionID, params.Body.Tags)
var prefix string
if params.Prefix != "" {
encodedPrefix := params.Prefix
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
return prepareError(err)
}
prefix = string(decodedPrefix)
}
err = putObjectTags(ctx, minioClient, params.BucketName, prefix, params.VersionID, params.Body.Tags)
if err != nil {
return prepareError(err)
}

View File

@@ -18,6 +18,7 @@ package restapi
import (
"crypto/rand"
"fmt"
"io"
"net/http"
"os"
@@ -105,22 +106,73 @@ func FileExists(filename string) bool {
return !info.IsDir()
}
func NewSessionCookieForConsole(token string) http.Cookie {
expiration := time.Now().Add(SessionDuration)
func NewSessionCookieForConsole(token string) []http.Cookie {
const CookieChunk = 3800
return http.Cookie{
Path: "/",
Name: "token",
Value: token,
MaxAge: int(SessionDuration.Seconds()), // 45 minutes
Expires: expiration,
HttpOnly: true,
// if len(GlobalPublicCerts) > 0 is true, that means Console is running with TLS enable and the browser
// should not leak any cookie if we access the site using HTTP
Secure: len(GlobalPublicCerts) > 0,
// read more: https://web.dev/samesite-cookies-explained/
SameSite: http.SameSiteLaxMode,
expiration := time.Now().Add(SessionDuration)
var cookies []http.Cookie
i := 0
cookieIndex := 0
for i < len(token) {
var until int
if i+CookieChunk < len(token) {
until = i + CookieChunk
} else {
until = len(token)
}
cookieName := "token"
if len(cookies) > 0 {
cookieName = fmt.Sprintf("token%d", len(cookies))
}
cookie := http.Cookie{
Path: "/",
Name: cookieName,
Value: token[i:until],
MaxAge: int(SessionDuration.Seconds()), // 45 minutes
Expires: expiration,
HttpOnly: true,
// if len(GlobalPublicCerts) > 0 is true, that means Console is running with TLS enable and the browser
// should not leak any cookie if we access the site using HTTP
Secure: len(GlobalPublicCerts) > 0,
// read more: https://web.dev/samesite-cookies-explained/
SameSite: http.SameSiteLaxMode,
}
cookies = append(cookies, cookie)
i += until
cookieIndex++
}
// clear old cookies
expiredDuration := time.Now().Add(-1 * time.Second)
for i := cookieIndex; i < 10; i++ {
cookieName := "token"
if len(cookies) > 0 {
cookieName = fmt.Sprintf("token%d", i)
}
cookie := http.Cookie{
Path: "/",
Name: cookieName,
Value: "",
MaxAge: 0, // 45 minutes
Expires: expiredDuration,
HttpOnly: true,
// if len(GlobalPublicCerts) > 0 is true, that means Console is running with TLS enable and the browser
// should not leak any cookie if we access the site using HTTP
Secure: len(GlobalPublicCerts) > 0,
// read more: https://web.dev/samesite-cookies-explained/
SameSite: http.SameSiteLaxMode,
}
cookies = append(cookies, cookie)
}
return cookies
}
func ExpireSessionCookie() http.Cookie {