Added speedtest page & updated diagnostic page (#1099)

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>
This commit is contained in:
Alex
2021-11-11 18:04:18 -06:00
committed by GitHub
parent 34dc51a579
commit 8102ab7e35
71 changed files with 2220 additions and 112 deletions

6
go.mod
View File

@@ -19,9 +19,9 @@ require (
github.com/minio/cli v1.22.0
github.com/minio/direct-csi v1.3.5-0.20210601185811-f7776f7961bf
github.com/minio/kes v0.11.0
github.com/minio/madmin-go v1.1.10
github.com/minio/mc v0.0.0-20211027024940-7866f97ef502
github.com/minio/minio-go/v7 v7.0.15-0.20211004160302-3b57c1e369ca
github.com/minio/madmin-go v1.1.12
github.com/minio/mc v0.0.0-20211110003602-1461b652d920
github.com/minio/minio-go/v7 v7.0.15
github.com/minio/operator v0.0.0-20211011212245-31460bbbc4b7
github.com/minio/operator/logsearchapi v0.0.0-20211011212245-31460bbbc4b7
github.com/minio/pkg v1.1.5

12
go.sum
View File

@@ -909,16 +909,18 @@ github.com/minio/filepath v1.0.0/go.mod h1:/nRZA2ldl5z6jT9/KQuvZcQlxZIMQoFFQPvEX
github.com/minio/kes v0.11.0 h1:8ma6OCVSxKT50b1uYXLJro3m7PmZtCLxBaTddQexI5k=
github.com/minio/kes v0.11.0/go.mod h1:mTF1Bv8YVEtQqF/B7Felp4tLee44Pp+dgI0rhCvgNg8=
github.com/minio/madmin-go v1.0.12/go.mod h1:BK+z4XRx7Y1v8SFWXsuLNqQqnq5BO/axJ8IDJfgyvfs=
github.com/minio/madmin-go v1.1.10 h1:pfMgXkzdwADnNfVdNMJbwok2fjb2sJ7Q76kDt89RGzE=
github.com/minio/madmin-go v1.1.10/go.mod h1:Iu0OnrMWNBYx1lqJTW+BFjBMx0Hi0wjw8VmqhiOs2Jo=
github.com/minio/mc v0.0.0-20211027024940-7866f97ef502 h1:7ip9qTspUniv+WDENgOcfUr95IccxG5aDkBM4Z96kQg=
github.com/minio/mc v0.0.0-20211027024940-7866f97ef502/go.mod h1:vxztwXLB9Gyl/h3Yh08Mpz1CB/0FO5Es0iQRpzxvS5I=
github.com/minio/madmin-go v1.1.11-0.20211102182201-e51fd3d6b104/go.mod h1:Iu0OnrMWNBYx1lqJTW+BFjBMx0Hi0wjw8VmqhiOs2Jo=
github.com/minio/madmin-go v1.1.12 h1:dtSPkOlzDa1Z2dnw9VQL0+OVVl+7O23o2lfztWs0Dqc=
github.com/minio/madmin-go v1.1.12/go.mod h1:Iu0OnrMWNBYx1lqJTW+BFjBMx0Hi0wjw8VmqhiOs2Jo=
github.com/minio/mc v0.0.0-20211110003602-1461b652d920 h1:3QhH/Ji1X6OsoQFkebmsU0D2R86bSpqm567xzWM7WtY=
github.com/minio/mc v0.0.0-20211110003602-1461b652d920/go.mod h1:V8NmUfU0W3G/mrifeO6nm4CWFTiXY2nx7FJyMge/aHk=
github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.11-0.20210302210017-6ae69c73ce78/go.mod h1:mTh2uJuAbEqdhMVl6CMIIZLUeiMiWtJR4JB8/5g2skw=
github.com/minio/minio-go/v7 v7.0.15-0.20211004160302-3b57c1e369ca h1:DKdUaXCMM6fFUwS9K68HGw8nlqqUZhQN106rPW1V/oI=
github.com/minio/minio-go/v7 v7.0.15-0.20211004160302-3b57c1e369ca/go.mod h1:pUV0Pc+hPd1nccgmzQF/EXh48l/Z/yps6QPF1aaie4g=
github.com/minio/minio-go/v7 v7.0.15 h1:r9/NhjJ+nXYrIYvbObhvc1wPj3YH1iDpJzz61uRKLyY=
github.com/minio/minio-go/v7 v7.0.15/go.mod h1:pUV0Pc+hPd1nccgmzQF/EXh48l/Z/yps6QPF1aaie4g=
github.com/minio/operator v0.0.0-20211011212245-31460bbbc4b7 h1:dkfuMNslMjGoJ4ArAMSoQhidYNdm3SgzLBP+f96O3/E=
github.com/minio/operator v0.0.0-20211011212245-31460bbbc4b7/go.mod h1:lDpuz8nwsfhKlfiBaA3Z8AW019fWEAjO2gltfLbdorE=
github.com/minio/operator/logsearchapi v0.0.0-20211011212245-31460bbbc4b7 h1:vFtQqCt67ETp0JAkOKRWTKkgwFv14Vc1jJSxmQ8wJE0=

View File

@@ -74,6 +74,7 @@ var (
tools = "/tools"
logs = "/tools/logs"
auditLogs = "/tools/audit-logs"
speedtest = "/tools/speedtest"
healthInfo = "/tools/diagnostics"
)
@@ -294,6 +295,16 @@ var healthInfoActionSet = ConfigurationActionSet{
),
}
// logsActionSet contains the list of admin actions required for this endpoint to work
var speedtestActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.HealthInfoAdminAction,
),
}
var displayRules = map[string]func() bool{
// disable users page if LDAP is enabled
users: func() bool {
@@ -344,6 +355,7 @@ var endpointRules = map[string]ConfigurationActionSet{
auditLogs: logsActionSet,
tools: toolsActionSet,
healthInfo: healthInfoActionSet,
speedtest: speedtestActionSet,
}
// operatorRules contains the mapping between endpoints and ActionSets for operator only mode

View File

@@ -70,7 +70,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:*",
},
},
want: 33,
want: 34,
},
{
name: "all s3 endpoints",
@@ -89,7 +89,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 35,
want: 36,
},
{
name: "Console User - default endpoints",

View File

@@ -1,23 +1,23 @@
{
"files": {
"main.css": "./static/css/main.ed78990a.chunk.css",
"main.js": "./static/js/main.894c8129.chunk.js",
"main.js.map": "./static/js/main.894c8129.chunk.js.map",
"main.js": "./static/js/main.f80bc34d.chunk.js",
"main.js.map": "./static/js/main.f80bc34d.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.71021f35.chunk.css": "./static/css/2.71021f35.chunk.css",
"static/js/2.0aaf1c4d.chunk.js": "./static/js/2.0aaf1c4d.chunk.js",
"static/js/2.0aaf1c4d.chunk.js.map": "./static/js/2.0aaf1c4d.chunk.js.map",
"static/js/2.05f3ee05.chunk.js": "./static/js/2.05f3ee05.chunk.js",
"static/js/2.05f3ee05.chunk.js.map": "./static/js/2.05f3ee05.chunk.js.map",
"index.html": "./index.html",
"static/css/2.71021f35.chunk.css.map": "./static/css/2.71021f35.chunk.css.map",
"static/css/main.ed78990a.chunk.css.map": "./static/css/main.ed78990a.chunk.css.map",
"static/js/2.0aaf1c4d.chunk.js.LICENSE.txt": "./static/js/2.0aaf1c4d.chunk.js.LICENSE.txt"
"static/js/2.05f3ee05.chunk.js.LICENSE.txt": "./static/js/2.05f3ee05.chunk.js.LICENSE.txt"
},
"entrypoints": [
"static/js/runtime-main.30f8243a.js",
"static/css/2.71021f35.chunk.css",
"static/js/2.0aaf1c4d.chunk.js",
"static/js/2.05f3ee05.chunk.js",
"static/css/main.ed78990a.chunk.css",
"static/js/main.894c8129.chunk.js"
"static/js/main.f80bc34d.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.71021f35.chunk.css" rel="stylesheet"><link href="./static/css/main.ed78990a.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.0aaf1c4d.chunk.js"></script><script src="./static/js/main.894c8129.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.71021f35.chunk.css" rel="stylesheet"><link href="./static/css/main.ed78990a.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.05f3ee05.chunk.js"></script><script src="./static/js/main.f80bc34d.chunk.js"></script></body></html>

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

@@ -24,6 +24,7 @@ const minMemReq = 2147483648; // Minimal Memory required for MinIO in bytes
export const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
export const k8sUnits = ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"];
export const k8sCalcUnits = ["B", ...k8sUnits];
export const timeUnits = ["ms", "s", "m", "h", "d", "w", "M", "Q", "y"];
export const niceBytes = (x: string, showK8sUnits: boolean = false) => {
let n = parseInt(x, 10) || 0;

View File

@@ -0,0 +1,52 @@
// 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 { SvgIcon } from "@mui/material";
interface IClusterIcon {
width?: number;
}
const ComputerLineIcon = ({ width = 24 }: IClusterIcon) => {
return (
<SvgIcon style={{ width: width, height: width }}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 38.888 29.791"
>
<g id="computer-line" transform="translate(-1 -5)">
<path
d="M9.833,24.9V10.833H26.791L28.6,9H8V24.9Z"
transform="translate(1.021 0.583)"
/>
<path
d="M6.292,7.292h27.5V25.624h2.292V6.719A1.719,1.719,0,0,0,34.363,5H5.719A1.719,1.719,0,0,0,4,6.719V25.624H6.292Z"
transform="translate(0.437)"
/>
<path
d="M1,25v3.9a2.979,2.979,0,0,0,2.979,2.979h32.93A2.979,2.979,0,0,0,39.888,28.9V25Zm36.665,3.9a.687.687,0,0,1-.687.687H3.933a.687.687,0,0,1-.687-.687V26.753h11.4A1.879,1.879,0,0,0,16.365,27.9h8.169a1.879,1.879,0,0,0,1.719-1.146H37.665Z"
transform="translate(0 2.916)"
/>
</g>
</svg>
</SvgIcon>
);
};
export default ComputerLineIcon;

View File

@@ -20,16 +20,15 @@ import { SvgIcon } from "@mui/material";
const CopyIcon = () => {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<title>ic_h_copy-new_sl</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
d="M0,0V16H16V0ZM11.886,9.048H9.048v2.838h-2.1V9.048H4.114v-2.1H6.952V4.114h2.1V6.952h2.838Z"
/>
</g>
</g>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.677 13.677">
<path
d="M41.764,15.9H34.3a1.4,1.4,0,0,0-1.4,1.4v7.46a1.4,1.4,0,0,0,1.4,1.4h7.46a1.4,1.4,0,0,0,1.4-1.4V17.3A1.4,1.4,0,0,0,41.764,15.9Zm.2,8.864a.2.2,0,0,1-.2.2H34.3a.2.2,0,0,1-.2-.2V17.3a.2.2,0,0,1,.2-.2h7.46a.2.2,0,0,1,.2.2Z"
transform="translate(-29.491 -15.9)"
/>
<path
d="M17.3,34.1h.441a.6.6,0,1,0,0-1.2H17.3a1.4,1.4,0,0,0-1.4,1.4v7.46a1.4,1.4,0,0,0,1.4,1.4h7.46a1.4,1.4,0,0,0,1.4-1.4v-.481a.6.6,0,0,0-1.2,0v.481a.2.2,0,0,1-.2.2H17.3a.2.2,0,0,1-.2-.2V34.3A.2.2,0,0,1,17.3,34.1Z"
transform="translate(-15.9 -29.491)"
/>
</svg>
</SvgIcon>
);

View File

@@ -0,0 +1,46 @@
// 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 "@mui/material";
const DownloadStatIcon = (props: SvgIconProps) => {
return (
<SvgIcon {...props}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 13">
<g transform="translate(0)">
<path
className="a"
fill="#4CCB92"
d="M1.966,8.119a.69.69,0,0,0,1.38,0V2.355l.782.781A.69.69,0,0,0,5.1,2.161L3.148.206a.69.69,0,0,0-.984,0L.21,2.161a.69.69,0,0,0,.975.975l.781-.781V8.119Z"
transform="translate(9.248 11.151) rotate(180)"
/>
<g
className="b"
stroke="#4CCB92"
fill="none"
transform="translate(0)"
>
<circle className="c" stroke="none" cx="6.5" cy="6.5" r="6.5" />
<circle className="d" fill="none" cx="6.5" cy="6.5" r="6" />
</g>
</g>
</svg>
</SvgIcon>
);
};
export default DownloadStatIcon;

View File

@@ -0,0 +1,73 @@
// 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 "@mui/material";
const JSONIcon = (props: SvgIconProps) => {
return (
<SvgIcon {...props}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24.726 20.753">
<g transform="translate(-1649.388 -543.732)">
<g transform="translate(1656.071 552.846)">
<circle
cx="1.5"
cy="1.5"
r="1.5"
transform="translate(-0.329 -0.295)"
/>
<circle
cx="1.5"
cy="1.5"
r="1.5"
transform="translate(8.671 -0.295)"
/>
<circle
cx="1.5"
cy="1.5"
r="1.5"
transform="translate(4.671 -0.295)"
/>
</g>
<g transform="translate(1649.588 543.932)">
<g transform="translate(17.829 0)">
<path
d="M13.681,0a.838.838,0,0,0,0,1.677,2.307,2.307,0,0,1,2.306,2.305V8.454a2.505,2.505,0,0,0,.685,1.722,2.505,2.505,0,0,0-.685,1.722v4.472a2.307,2.307,0,0,1-2.306,2.306.838.838,0,0,0,0,1.677,3.986,3.986,0,0,0,3.982-3.982V11.9a.839.839,0,0,1,.838-.838.838.838,0,0,0,.838-.838c0-.015,0-.03,0-.046s0-.03,0-.046a.838.838,0,0,0-.838-.838.839.839,0,0,1-.838-.838V3.982A3.987,3.987,0,0,0,13.681,0Z"
transform="translate(-12.843 0)"
/>
<path
d="M13.681-.2a4.187,4.187,0,0,1,4.182,4.182V8.454a.639.639,0,0,0,.638.638,1.039,1.039,0,0,1,1.038,1.038c0,.015,0,.031,0,.046s0,.03,0,.046A1.04,1.04,0,0,1,18.5,11.261a.639.639,0,0,0-.638.638v4.472a4.187,4.187,0,0,1-4.182,4.182,1.038,1.038,0,0,1,0-2.077,2.108,2.108,0,0,0,2.106-2.106V11.9a2.7,2.7,0,0,1,.619-1.722,2.7,2.7,0,0,1-.619-1.722V3.982a2.108,2.108,0,0,0-2.106-2.105,1.038,1.038,0,0,1,0-2.077Zm0,20.353a3.787,3.787,0,0,0,3.782-3.782V11.9A1.04,1.04,0,0,1,18.5,10.861a.639.639,0,0,0,.638-.638c0-.012,0-.023,0-.035v-.021c0-.012,0-.023,0-.035a.639.639,0,0,0-.638-.638,1.04,1.04,0,0,1-1.038-1.038V3.982A3.786,3.786,0,0,0,13.681.2a.638.638,0,0,0,0,1.277,2.508,2.508,0,0,1,2.506,2.505V8.454a2.3,2.3,0,0,0,.631,1.585l.129.137-.129.137a2.3,2.3,0,0,0-.631,1.585v4.472a2.508,2.508,0,0,1-2.506,2.506.638.638,0,0,0,0,1.277Z"
transform="translate(-12.843 0)"
/>
</g>
<g transform="translate(0 0)">
<path
d="M18.5,0a.838.838,0,0,1,0,1.677A2.307,2.307,0,0,0,16.2,3.982V8.454a2.505,2.505,0,0,1-.685,1.722A2.505,2.505,0,0,1,16.2,11.9v4.472A2.307,2.307,0,0,0,18.5,18.676a.838.838,0,0,1,0,1.677,3.986,3.986,0,0,1-3.982-3.982V11.9a.839.839,0,0,0-.838-.838.838.838,0,0,1-.838-.838c0-.015,0-.03,0-.046s0-.03,0-.046a.838.838,0,0,1,.838-.838.839.839,0,0,0,.838-.838V3.982A3.987,3.987,0,0,1,18.5,0Z"
transform="translate(-12.843 0)"
/>
<path
d="M18.5-.2a1.038,1.038,0,0,1,0,2.077A2.108,2.108,0,0,0,16.4,3.982V8.454a2.7,2.7,0,0,1-.619,1.722A2.7,2.7,0,0,1,16.4,11.9v4.472A2.108,2.108,0,0,0,18.5,18.476a1.038,1.038,0,0,1,0,2.077,4.187,4.187,0,0,1-4.182-4.182V11.9a.639.639,0,0,0-.638-.638,1.04,1.04,0,0,1-1.038-1.038c0-.015,0-.031,0-.046s0-.03,0-.046a1.04,1.04,0,0,1,1.038-1.038.639.639,0,0,0,.638-.638V3.982A4.187,4.187,0,0,1,18.5-.2Zm0,20.353a.638.638,0,0,0,0-1.277A2.508,2.508,0,0,1,16,16.371V11.9a2.3,2.3,0,0,0-.631-1.585l-.129-.137.129-.137A2.3,2.3,0,0,0,16,8.454V3.982A2.508,2.508,0,0,1,18.5,1.477.638.638,0,0,0,18.5.2a3.787,3.787,0,0,0-3.782,3.782V8.454a1.04,1.04,0,0,1-1.038,1.038.639.639,0,0,0-.638.638c0,.012,0,.024,0,.035v.021c0,.012,0,.023,0,.035a.639.639,0,0,0,.638.638A1.04,1.04,0,0,1,14.719,11.9v4.472A3.787,3.787,0,0,0,18.5,20.153Z"
transform="translate(-12.843 0)"
/>
</g>
</g>
</g>
</svg>
</SvgIcon>
);
};
export default JSONIcon;

View File

@@ -0,0 +1,33 @@
// 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 "@mui/material";
const NextArrowIcon = (props: SvgIconProps) => {
return (
<SvgIcon {...props}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12.621 7.62">
<path
d="M2.821,11.646a.989.989,0,0,0,1.979,0V3.378L5.92,4.5a.99.99,0,0,0,1.4-1.4L4.515.3A.989.989,0,0,0,3.1.3L.3,3.1A.989.989,0,0,0,1.7,4.5L2.821,3.378v8.268Z"
transform="translate(12.621) rotate(90)"
/>
</svg>
</SvgIcon>
);
};
export default NextArrowIcon;

View File

@@ -0,0 +1,109 @@
// 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 "@mui/material";
const SpeedtestIcon = (props: SvgIconProps) => {
return (
<SvgIcon {...props}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<g id="speedtest-icn" transform="translate(-909.185 -1023.223)">
<path
id="Trazado_426"
data-name="Trazado 426"
d="M1032.321,1146.118l-.1.084a5.326,5.326,0,0,0,3.505,9.344l-.011.063a5.319,5.319,0,0,0,3.516-1.371l.1-.084q.167-.135.322-.281a5.337,5.337,0,1,0-7.333-7.756Z"
transform="translate(-59.722 -59.838)"
/>
<path
id="Trazado_427"
data-name="Trazado 427"
d="M999.251,1197.047a4.336,4.336,0,0,0-5.884,1.729v.095a4.336,4.336,0,0,0,3.817,6.344l-.011.01a4.361,4.361,0,0,0,2.078-8.178Z"
transform="translate(-41.238 -85.391)"
/>
<path
id="Trazado_428"
data-name="Trazado 428"
d="M1152.562,1112.162h.293a7.816,7.816,0,1,0-.046-15.631h-.247a7.816,7.816,0,0,0,0,15.631Z"
transform="translate(-116.086 -36.123)"
/>
<path
id="Trazado_429"
data-name="Trazado 429"
d="M979.368,1259.216l-.028,0a2.958,2.958,0,0,0-3.324,2.541v.08a2.973,2.973,0,0,0,2.559,3.336,3.173,3.173,0,0,0,.379,0l-.021.007a2.972,2.972,0,0,0,2.959-2.558v-.056A2.966,2.966,0,0,0,979.368,1259.216Z"
transform="translate(-32.919 -116.272)"
/>
<path
id="Trazado_430"
data-name="Trazado 430"
d="M1085.813,1109.608l-.209.078a7.07,7.07,0,0,0,2.5,13.688l-.022.065a7.009,7.009,0,0,0,2.537-.529l.165-.066.1-.039a7.07,7.07,0,1,0-5.076-13.2Z"
transform="translate(-84.673 -42.333)"
/>
<path
id="Trazado_431"
data-name="Trazado 431"
d="M1271.935,1152.822a9.817,9.817,0,0,0-.929-13.852l-.268-.235a9.817,9.817,0,0,0-12.881,14.8l.246.212a9.806,9.806,0,0,0,6.452,2.426h0A9.815,9.815,0,0,0,1271.935,1152.822Z"
transform="translate(-170.269 -55.836)"
/>
<path
id="Trazado_432"
data-name="Trazado 432"
d="M1313.471,1236.2h0Z"
transform="translate(-199.154 -104.944)"
/>
<path
id="Trazado_433"
data-name="Trazado 433"
d="M1333.353,1250.845l-.067-.495a12.786,12.786,0,0,0-12.612-11.007,12.761,12.761,0,0,0-12.638,14.485v.428a12.786,12.786,0,0,0,12.612,11.047,13.068,13.068,0,0,0,1.778-.12A12.76,12.76,0,0,0,1333.353,1250.845Z"
transform="translate(-196.477 -106.494)"
/>
<path
id="Trazado_434"
data-name="Trazado 434"
d="M1307.5,1203.557a11.283,11.283,0,0,0,4.537-15.3l-.2-.361c-.086-.167-.176-.332-.27-.5a11.283,11.283,0,1,0-19.545,11.281l.187.336a11.278,11.278,0,0,0,15.289,4.538Z"
transform="translate(-187.898 -78.119)"
/>
<path
id="Trazado_435"
data-name="Trazado 435"
d="M1214.118,1106.631l-.289-.111a8.657,8.657,0,1,0-6.052,16.222l.255.1a8.643,8.643,0,0,0,3.048.556l-.01.066a8.7,8.7,0,0,0,3.048-16.833Z"
transform="translate(-144.357 -40.775)"
/>
<path
id="Trazado_436"
data-name="Trazado 436"
d="M1165.325,1242.488l-13.839,11.867a.333.333,0,0,1-.331,0,17.171,17.171,0,1,0,10.435,12.167.333.333,0,0,1,0-.316l13.9-11.866a7.807,7.807,0,0,0-10.165-11.851Zm-12.039,27.588a8.26,8.26,0,1,1-8.26-8.26A8.26,8.26,0,0,1,1153.286,1270.075Z"
transform="translate(-107.706 -107.117)"
/>
<path
id="Trazado_437"
data-name="Trazado 437"
d="M1158.132,1407.172h-20.3a3.765,3.765,0,0,0,0,7.53h20.33a3.764,3.764,0,0,0,3.764-3.765v-.03A3.765,3.765,0,0,0,1158.132,1407.172Z"
transform="translate(-110.812 -189.193)"
/>
<path
id="Trazado_438"
data-name="Trazado 438"
d="M1037.185,1023.223a128,128,0,1,0,128,128A128.15,128.15,0,0,0,1037.185,1023.223Zm0,233.412A105.412,105.412,0,1,1,1142.6,1151.223,105.412,105.412,0,0,1,1037.185,1256.635Z"
transform="translate(0 0)"
/>
</g>
</svg>
</SvgIcon>
);
};
export default SpeedtestIcon;

View File

@@ -0,0 +1,46 @@
// 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 "@mui/material";
const DownloadStatIcon = (props: SvgIconProps) => {
return (
<SvgIcon {...props}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 13">
<g transform="translate(0)">
<path
className="a"
fill="#2781B0"
d="M1.966,8.119a.69.69,0,0,0,1.38,0V2.355l.782.781A.69.69,0,0,0,5.1,2.161L3.148.206a.69.69,0,0,0-.984,0L.21,2.161a.69.69,0,0,0,.975.975l.781-.781V8.119Z"
transform="translate(3.851 2.12)"
/>
<g
className="b"
stroke="#2781B0"
fill="none"
transform="translate(0)"
>
<circle className="c" stroke="none" cx="6.5" cy="6.5" r="6.5" />
<circle className="d" fill="none" cx="6.5" cy="6.5" r="6" />
</g>
</g>
</svg>
</SvgIcon>
);
};
export default DownloadStatIcon;

View File

@@ -105,3 +105,8 @@ export { default as OpenListIcon } from "./OpenListIcon";
export { default as ToolsIcon } from "./ToolsIcon";
export { default as RecoverIcon } from "./RecoverIcon";
export { default as PrometheusIcon } from "./PrometheusIcon";
export { default as NextArrowIcon } from "./NextArrowIcon";
export { default as DownloadStatIcon } from "./DownloadStatIcon";
export { default as UploadStatIcon } from "./UploadStatIcon";
export { default as ComputerLineIcon } from "./ComputerLineIcon";
export { default as JSONIcon } from "./JSONIcon";

View File

@@ -40,7 +40,7 @@ import {
ADMIN_LIST_USER_POLICIES,
ADMIN_LIST_USERS,
} from "../../../../types";
import PanelTitle from "../../Common/PanelTitle";
import PanelTitle from "../../Common/PanelTitle/PanelTitle";
const styles = (theme: Theme) => createStyles({});

View File

@@ -40,7 +40,7 @@ import {
import { BucketInfo } from "../types";
import { displayComponent } from "../../../../utils/permissions";
import { S3_GET_BUCKET_POLICY, S3_PUT_BUCKET_POLICY } from "../../../../types";
import PanelTitle from "../../Common/PanelTitle";
import PanelTitle from "../../Common/PanelTitle/PanelTitle";
const styles = (theme: Theme) =>
createStyles({

View File

@@ -53,7 +53,7 @@ import { BucketsIcon, DeleteIcon, FolderIcon } from "../../../../icons";
import DeleteBucket from "../ListBuckets/DeleteBucket";
import AccessRulePanel from "./AccessRulePanel";
import RefreshIcon from "../../../../icons/RefreshIcon";
import BoxIconButton from "../../Common/BoxIconButton";
import BoxIconButton from "../../Common/BoxIconButton/BoxIconButton";
import {
ADMIN_GET_POLICY,
ADMIN_LIST_USER_POLICIES,

View File

@@ -42,7 +42,7 @@ import {
S3_GET_BUCKET_NOTIFICATIONS,
S3_PUT_BUCKET_NOTIFICATIONS,
} from "../../../../types";
import PanelTitle from "../../Common/PanelTitle";
import PanelTitle from "../../Common/PanelTitle/PanelTitle";
const styles = (theme: Theme) =>
createStyles({

View File

@@ -43,7 +43,7 @@ import {
S3_GET_LIFECYCLE_CONFIGURATION,
S3_PUT_LIFECYCLE_CONFIGURATION,
} from "../../../../types";
import PanelTitle from "../../Common/PanelTitle";
import PanelTitle from "../../Common/PanelTitle/PanelTitle";
const styles = (theme: Theme) =>
createStyles({

View File

@@ -45,7 +45,7 @@ import {
S3_GET_REPLICATION_CONFIGURATION,
S3_PUT_REPLICATION_CONFIGURATION,
} from "../../../../types";
import PanelTitle from "../../Common/PanelTitle";
import PanelTitle from "../../Common/PanelTitle/PanelTitle";
interface IBucketReplicationProps {
classes: any;

View File

@@ -68,7 +68,7 @@ import {
S3_PUT_BUCKET_VERSIONING,
S3_PUT_OBJECT_RETENTION,
} from "../../../../types";
import PanelTitle from "../../Common/PanelTitle";
import PanelTitle from "../../Common/PanelTitle/PanelTitle";
interface IBucketSummaryProps {
classes: any;

View File

@@ -43,9 +43,9 @@ import { ISessionResponse } from "../../types";
import TextField from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import SearchIcon from "../../../../icons/SearchIcon";
import BoxIconButton from "../../Common/BoxIconButton";
import BoxIconButton from "../../Common/BoxIconButton/BoxIconButton";
import RefreshIcon from "../../../../icons/RefreshIcon";
import AButton from "../../Common/AButton";
import AButton from "../../Common/AButton/AButton";
const styles = (theme: Theme) =>
createStyles({

View File

@@ -102,7 +102,7 @@ import {
} from "../../../../../../types";
import { setBucketDetailsLoad, setBucketInfo } from "../../../actions";
import { AppState } from "../../../../../../store";
import BoxIconButton from "../../../../Common/BoxIconButton";
import BoxIconButton from "../../../../Common/BoxIconButton/BoxIconButton";
const commonIcon = {
backgroundRepeat: "no-repeat",

View File

@@ -93,7 +93,7 @@ import SearchIcon from "../../../../../../icons/SearchIcon";
import ObjectBrowserIcon from "../../../../../../icons/ObjectBrowserIcon";
import PreviewFileContent from "../Preview/PreviewFileContent";
import RestoreFileVersion from "./RestoreFileVersion";
import BoxIconButton from "../../../../Common/BoxIconButton";
import BoxIconButton from "../../../../Common/BoxIconButton/BoxIconButton";
import { RecoverIcon } from "../../../../../../icons";
const styles = (theme: Theme) =>

View File

@@ -55,6 +55,7 @@ interface InputBoxProps {
max?: string;
overlayIcon?: any;
overlayAction?: () => void;
overlayObject?: any;
extraInputProps?: StandardInputProps["inputProps"];
noLabelMinWidth?: boolean;
}
@@ -126,6 +127,7 @@ const InputBoxWrapper = ({
min,
max,
overlayIcon = null,
overlayObject = null,
extraInputProps = {},
overlayAction,
noLabelMinWidth = false,
@@ -214,6 +216,15 @@ const InputBoxWrapper = ({
</IconButton>
</div>
)}
{overlayObject && (
<div
className={`${classes.overlayAction} ${
label !== "" ? "withLabel" : ""
}`}
>
{overlayObject}
</div>
)}
</Grid>
</React.Fragment>
);

View File

@@ -0,0 +1,107 @@
// 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 { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { selectorTypes } from "../SelectWrapper/SelectWrapper";
import { Menu, MenuItem } from "@mui/material";
interface IInputUnitBox {
classes: any;
id: string;
unitSelected: string;
unitsList: selectorTypes[];
disabled?: boolean;
onUnitChange: (newValue: string) => void;
}
const styles = (theme: Theme) =>
createStyles({
buttonTrigger: {
border: "#F0F2F2 1px solid",
borderRadius: 3,
color: "#838383",
backgroundColor: "#fff",
fontSize: 12,
},
});
const InputUnitMenu = ({
classes,
id,
unitSelected,
unitsList,
disabled = false,
onUnitChange,
}: IInputUnitBox) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = (newUnit: string) => {
setAnchorEl(null);
if (newUnit !== "") {
onUnitChange(newUnit);
}
};
return (
<Fragment>
<button
id={`${id}-button`}
aria-controls={`${id}-menu`}
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
onClick={handleClick}
className={classes.buttonTrigger}
disabled={disabled}
>
{unitSelected}
</button>
<Menu
id={`${id}-menu`}
aria-labelledby={`${id}-button`}
anchorEl={anchorEl}
open={open}
onClose={() => {
handleClose("");
}}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
>
{unitsList.map((unit) => (
<MenuItem
onClick={() => handleClose(unit.value)}
key={`itemUnit-${unit.value}-${unit.label}`}
>
{unit.label}
</MenuItem>
))}
</Menu>
</Fragment>
);
};
export default withStyles(styles)(InputUnitMenu);

View File

@@ -987,3 +987,13 @@ export const commonDashboardInfocard = {
},
},
};
export const linkStyles = (color: string) => ({
link: {
textDecoration: "underline",
color,
backgroundColor: "transparent",
border: 0,
cursor: "pointer",
},
});

View File

@@ -0,0 +1,49 @@
// 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 { styled } from "@mui/material/styles";
import LinearProgress, {
linearProgressClasses,
} from "@mui/material/LinearProgress";
interface IProgressBarWrapper {
value: number;
ready: boolean;
}
const BorderLinearProgress = styled(LinearProgress)(() => ({
height: 10,
borderRadius: 5,
[`&.${linearProgressClasses.colorPrimary}`]: {
backgroundColor: "#f1f1f1",
},
[`& .${linearProgressClasses.bar}`]: {
borderRadius: 5,
},
}));
const ProgressBarWrapper = ({ value, ready }: IProgressBarWrapper) => {
return (
<BorderLinearProgress
variant="determinate"
value={value}
color={ready ? "success" : "primary"}
/>
);
};
export default ProgressBarWrapper;

View File

@@ -0,0 +1,235 @@
// 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 } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { CircularProgress, Grid } from "@mui/material";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import { DrivesIcon, VersionIcon } from "../../../../icons";
import { ServerInfo, Usage } from "../../Dashboard/types";
import { ErrorResponseHandler } from "../../../../common/types";
import api from "../../../../common/api";
interface ITestWrapper {
title: any;
children: any;
classes: any;
advancedVisible: boolean;
advancedContent?: any;
}
const styles = (theme: Theme) =>
createStyles({
titleBar: {
borderBottom: "#E5E5E5 1px solid",
padding: "30px 25px",
fontSize: 20,
color: "#07193E",
fontWeight: "bold",
borderRadius: "10px 10px 0px 0px",
paddingTop: 0,
},
divisorContainer: {
padding: 25,
},
serversData: {
color: "#07193E",
fontSize: 18,
display: "flex",
alignItems: "center",
"& svg": {
marginRight: 10,
},
},
minioVersionContainer: {
fontSize: 12,
color: "#07193E",
justifyContent: "center",
alignSelf: "center",
alignItems: "center",
display: "flex",
},
versionIcon: {
color: "#07193E",
marginRight: 20,
},
loaderAlign: {
textAlign: "center",
},
advancedContainer: {
justifyContent: "flex-end",
display: "flex",
},
optionsContainer: {
padding: 0,
marginBottom: 25,
},
advancedConfiguration: {
color: "#2781B0",
fontSize: 10,
textDecoration: "underline",
border: "none",
backgroundColor: "transparent",
cursor: "pointer",
alignItems: "center",
display: "flex",
"&:hover": {
color: "#07193E",
},
"& svg": {
width: 10,
alignSelf: "center",
marginLeft: 5,
},
},
advancedOpen: {
transform: "rotateZ(-90deg) translateX(-4px) translateY(2px)",
},
advancedClosed: {
transform: "rotateZ(90deg)",
},
advancedContent: {
backgroundColor: "#F5F7F9",
maxHeight: 0,
transitionDuration: "0.3s",
overflow: "hidden",
padding: "0 15px",
"&.open": {
maxHeight: 400,
padding: 15,
},
},
});
const TestWrapper = ({
title,
children,
classes,
advancedVisible,
advancedContent,
}: ITestWrapper) => {
const [version, setVersion] = useState<string>("N/A");
const [totalNodes, setTotalNodes] = useState<number>(0);
const [totalDrives, setTotalDrives] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(true);
const [advancedOpen, setAdvancedOpen] = useState<boolean>(false);
useEffect(() => {
if (loading) {
api
.invoke("GET", `/api/v1/admin/info?defaultOnly=true`)
.then((res: Usage) => {
const totalServers = res.servers?.length;
setTotalNodes(totalServers);
if (res.servers.length > 0) {
setVersion(res.servers[0].version);
const totalServers = res.servers.reduce(
(prevTotal: number, currentElement: ServerInfo) => {
return prevTotal + currentElement.drives.length;
},
0
);
setTotalDrives(totalServers);
}
setLoading(false);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
});
}
}, [loading]);
return (
<Grid item xs={12}>
<Grid item xs={12} className={classes.titleBar}>
{title}
</Grid>
<Grid item xs={12}>
<Grid item xs={12} className={classes.optionsContainer}>
<Grid container className={classes.divisorContainer}>
{!loading ? (
<Fragment>
<Grid item xs={12} md={4} className={classes.serversData}>
<DrivesIcon /> <strong>{totalNodes}</strong>
&nbsp;nodes,&nbsp;
<strong>{totalDrives}</strong>&nbsp; drives
</Grid>
<Grid
item
xs={12}
md={4}
className={classes.minioVersionContainer}
>
<span className={classes.versionIcon}>
<VersionIcon />
</span>{" "}
MinIO VERSION&nbsp;<strong>{version}</strong>
</Grid>
<Grid item xs={12} md={4} className={classes.advancedContainer}>
{advancedVisible && (
<button
onClick={() => {
setAdvancedOpen(!advancedOpen);
}}
className={classes.advancedConfiguration}
>
Advanced configurations{" "}
<span
className={
advancedOpen
? classes.advancedOpen
: classes.advancedClosed
}
>
<ArrowForwardIosIcon />
</span>
</button>
)}
</Grid>
</Fragment>
) : (
<Fragment>
<Grid item xs={12} className={classes.loaderAlign}>
<CircularProgress size={25} />
</Grid>
</Fragment>
)}
</Grid>
{advancedContent && (
<Grid
xs={12}
className={`${classes.advancedContent} ${
advancedOpen ? "open" : ""
}`}
>
{advancedContent}
</Grid>
)}
</Grid>
{children}
</Grid>
</Grid>
);
};
export default withStyles(styles)(TestWrapper);

View File

@@ -43,8 +43,8 @@ import RefreshIcon from "../../../../icons/RefreshIcon";
import SearchIcon from "../../../../icons/SearchIcon";
import PageHeader from "../../Common/PageHeader/PageHeader";
import HelpBox from "../../../../common/HelpBox";
import BoxIconButton from "../../Common/BoxIconButton";
import AButton from "../../Common/AButton";
import BoxIconButton from "../../Common/BoxIconButton/BoxIconButton";
import AButton from "../../Common/AButton/AButton";
interface IListTiersConfig {
classes: any;

View File

@@ -66,6 +66,7 @@ import Tools from "./Tools/Tools";
import ErrorLogs from "./Logs/ErrorLogs/ErrorLogs";
import LogsSearchMain from "./Logs/LogSearch/LogsSearchMain";
import GroupsDetails from "./Groups/GroupsDetails";
import Speedtest from "./Speedtest/Speedtest";
const drawerWidth = 245;
@@ -250,6 +251,10 @@ const Console = ({
component: Watch,
path: "/tools/watch",
},
{
component: Speedtest,
path: "/tools/speedtest",
},
{
component: Users,
path: "/users/:userName+",

View File

@@ -43,7 +43,7 @@ import FormatDrives from "./FormatDrives";
import FormatErrorsResult from "./FormatErrorsResult";
import RefreshIcon from "../../../icons/RefreshIcon";
import SearchIcon from "../../../icons/SearchIcon";
import BoxIconButton from "../Common/BoxIconButton";
import BoxIconButton from "../Common/BoxIconButton/BoxIconButton";
interface IDirectCSIMain {
classes: any;

View File

@@ -42,7 +42,7 @@ import PageHeader from "../Common/PageHeader/PageHeader";
import SearchIcon from "../../../icons/SearchIcon";
import HelpBox from "../../../common/HelpBox";
import history from "../../../history";
import AButton from "../Common/AButton";
import AButton from "../Common/AButton/AButton";
interface IGroupsProps {
classes: any;

View File

@@ -28,7 +28,7 @@ import SetPolicy from "../Policies/SetPolicy";
import AddGroupMember from "./AddGroupMember";
import { ErrorResponseHandler } from "../../../common/types";
import DeleteGroup from "./DeleteGroup";
import PanelTitle from "../Common/PanelTitle";
import PanelTitle from "../Common/PanelTitle/PanelTitle";
const styles = (theme: Theme) =>
createStyles({

View File

@@ -13,7 +13,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, useState, useEffect } from "react";
import {
ICloseEvent,
IMessageEvent,
@@ -46,6 +46,7 @@ import PageHeader from "../Common/PageHeader/PageHeader";
import { setServerDiagStat, setSnackBarMessage } from "../../../actions";
import CircularProgress from "@mui/material/CircularProgress";
import BackLink from "../../../common/BackLink";
import TestWrapper from "../Common/TestWrapper/TestWrapper";
const styles = (theme: Theme) =>
createStyles({
@@ -73,6 +74,26 @@ const styles = (theme: Theme) =>
padding: 40,
backgroundColor: "#fff",
},
localMessage: {
fontSize: 24,
color: "#07193E",
fontWeight: "bold",
textAlign: "center",
marginBottom: 10,
},
startDiagnostic: {
textAlign: "center",
marginBottom: 25,
},
progressResult: {
textAlign: "center",
marginBottom: 25,
},
diagNew: {
textAlign: "right",
margin: 25,
marginBottom: 0,
},
...actionsTray,
...containerForHeader(theme.spacing(4)),
});
@@ -115,7 +136,27 @@ const HealthInfo = ({
serverDiagnosticStatus,
}: IHealthInfo) => {
const [startDiagnostic, setStartDiagnostic] = useState(false);
const [diagStarted, setDiagStarted] = useState<boolean>(false);
const [downloadDisabled, setDownloadDisabled] = useState(true);
const [localMessage, setMessage] = useState<string>("");
const [title, setTitle] = useState<string>("Start new Diagnostic");
useEffect(() => {
if (serverDiagnosticStatus === DiagStatInProgress) {
setTitle("Diagnostic in progress...");
return;
}
if (serverDiagnosticStatus === DiagStatSuccess && diagStarted) {
setTitle("Diagnostic complete");
return;
}
if (serverDiagnosticStatus === DiagStatError) {
setTitle("Error");
return;
}
}, [serverDiagnosticStatus, startDiagnostic, diagStarted]);
useEffect(() => {
if (
@@ -155,7 +196,8 @@ const HealthInfo = ({
interval = setInterval(() => {
c.send("ok");
}, 10 * 1000);
setSnackBarMessage(
setDiagStarted(true);
setMessage(
"Diagnostic started. Please do not refresh page during diagnosis."
);
setServerDiagStat(DiagStatInProgress);
@@ -163,6 +205,7 @@ const HealthInfo = ({
c.onmessage = (message: IMessageEvent) => {
let m: HealthInfoMessage = JSON.parse(message.data.toString());
m.timestamp = new Date(m.timestamp.toString());
healthInfoMessageReceived(m);
};
c.onerror = (error: Error) => {
@@ -180,13 +223,12 @@ const HealthInfo = ({
) {
// handle close with error
console.log("connection closed by server with code:", event.code);
setSnackBarMessage(
"An error occurred while getting Diagnostic file."
);
setMessage("An error occurred while getting Diagnostic file.");
setServerDiagStat(DiagStatError);
} else {
console.log("connection closed by server");
setSnackBarMessage("Diagnostic file is ready to be downloaded.");
setMessage("Diagnostic file is ready to be downloaded.");
setServerDiagStat(DiagStatSuccess);
}
};
@@ -204,7 +246,7 @@ const HealthInfo = ({
]);
return (
<React.Fragment>
<Fragment>
<PageHeader label="Diagnostic" />
<Grid container className={classes.container}>
@@ -212,44 +254,76 @@ const HealthInfo = ({
<BackLink to="/tools" label="Return to Tools" />
</Grid>
<Grid item xs={12} className={classes.boxy}>
<Grid container className={classes.buttons}>
<Grid key="start-diag" item>
<Button
type="submit"
variant="contained"
color="primary"
disabled={startDiagnostic}
onClick={() => setStartDiagnostic(true)}
>
Start Diagnostic
</Button>
</Grid>
<Grid key="start-download" item>
{serverDiagnosticStatus === DiagStatInProgress ? (
<div className={classes.loading}>
<CircularProgress size={25} />
</div>
) : (
<Button
type="submit"
variant="contained"
color="primary"
onClick={() => {
download(
"diagnostic.json",
JSON.stringify(message, null, 2)
);
}}
disabled={downloadDisabled}
<TestWrapper title={title} advancedVisible={false}>
<Grid container className={classes.buttons}>
{!diagStarted && (
<Grid
key="start-diag"
item
xs={12}
className={classes.startDiagnostic}
>
Download
</Button>
<Button
type="submit"
variant="contained"
color="primary"
disabled={startDiagnostic}
onClick={() => setStartDiagnostic(true)}
>
Start Diagnostic
</Button>
</Grid>
)}
{diagStarted && (
<Grid
key="start-download"
item
xs={12}
className={classes.progressResult}
>
<div className={classes.localMessage}>{localMessage}</div>
{serverDiagnosticStatus === DiagStatInProgress ? (
<div className={classes.loading}>
<CircularProgress size={25} />
</div>
) : (
<Fragment>
{serverDiagnosticStatus !== DiagStatError && (
<Button
type="submit"
variant="contained"
color="primary"
onClick={() => {
download(
"diagnostic.json",
JSON.stringify(message, null, 2)
);
}}
disabled={downloadDisabled}
>
Download
</Button>
)}
<Grid item xs={12} className={classes.diagNew}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={startDiagnostic}
onClick={() => setStartDiagnostic(true)}
>
Start new Diagnostic
</Button>
</Grid>
</Fragment>
)}
</Grid>
)}
</Grid>
</Grid>
</TestWrapper>
</Grid>
</Grid>
</React.Fragment>
</Fragment>
);
};

View File

@@ -18,6 +18,7 @@ import React from "react";
import { connect } from "react-redux";
import { NavLink } from "react-router-dom";
import { Divider, Drawer, IconButton, Tooltip } from "@mui/material";
import { ChevronLeft } from "@mui/icons-material";
import withStyles from "@mui/styles/withStyles";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
@@ -25,6 +26,7 @@ import ListItem from "@mui/material/ListItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import List from "@mui/material/List";
import clsx from "clsx";
import { AppState } from "../../../store";
import { setMenuOpen, userLoggedIn } from "../../../actions";
import { menuGroups } from "./utils";
@@ -32,6 +34,7 @@ import { IMenuItem } from "./types";
import {
BucketsIcon,
DashboardIcon,
DiagnosticsIcon,
GroupsIcon,
IAMPoliciesIcon,
LambdaIcon,
@@ -56,9 +59,6 @@ import StorageIcon from "../../../icons/StorageIcon";
import TenantsOutlinedIcon from "../../../icons/TenantsOutlineIcon";
import MenuIcon from "@mui/icons-material/Menu";
import clsx from "clsx";
import { ChevronLeft } from "@mui/icons-material";
const drawerWidth = 245;
const styles = (theme: Theme) =>
@@ -396,7 +396,14 @@ const Menu = ({
icon: <HealIcon />,
fsHidden: distributedSetup,
},
{
group: "Tools",
type: "item",
component: NavLink,
to: "/health-info",
name: "Diagnostic",
icon: <DiagnosticsIcon />,
},
{
group: "Operator",
type: "item",

View File

@@ -46,8 +46,8 @@ import RefreshIcon from "../../../icons/RefreshIcon";
import SearchIcon from "../../../icons/SearchIcon";
import history from "../../../history";
import HelpBox from "../../../common/HelpBox";
import BoxIconButton from "../Common/BoxIconButton";
import AButton from "../Common/AButton";
import BoxIconButton from "../Common/BoxIconButton/BoxIconButton";
import AButton from "../Common/AButton/AButton";
interface IListNotificationEndpoints {
classes: any;

View File

@@ -47,7 +47,7 @@ import IAMPoliciesIcon from "../../../icons/IAMPoliciesIcon";
import RefreshIcon from "../../../icons/RefreshIcon";
import SearchIcon from "../../../icons/SearchIcon";
import TrashIcon from "../../../icons/TrashIcon";
import BoxIconButton from "../Common/BoxIconButton";
import BoxIconButton from "../Common/BoxIconButton/BoxIconButton";
interface IPolicyDetailsProps {
classes: any;

View File

@@ -0,0 +1,519 @@
// 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 } from "react";
import get from "lodash/get";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Grid } from "@mui/material";
import { IndvServerMetric, SpeedTestResponse, STServer } from "./types";
import { calculateBytes, prettyNumber } from "../../../common/utils";
import {
ComputerLineIcon,
DownloadIcon,
DownloadStatIcon,
JSONIcon,
StorageIcon,
UploadStatIcon,
VersionIcon,
} from "../../../icons";
import BoxIconButton from "../Common/BoxIconButton/BoxIconButton";
import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer } from "recharts";
import { cleanMetrics } from "./utils";
interface ISTResults {
classes: any;
results: SpeedTestResponse[];
}
const styles = (theme: Theme) =>
createStyles({
statContainer: {
padding: "18px 0 18px 25px",
},
statBlock: {
border: "#EEF1F4 1px solid",
borderRadius: 4,
},
testedAmount: {
display: "flex",
justifyContent: "space-between",
color: "#07193E",
padding: "12px 18px",
border: "#EEF1F4 1px solid",
borderLeft: 0,
borderRight: 0,
},
serverLength: {
color: "#696969",
},
serverDescrContainer: {
display: "flex",
margin: "15px 5px 0",
position: "relative",
},
serverDescrIcon: {
marginRight: 10,
"& svg": {
width: 16,
},
},
serverDescriptor: {
color: "#696969",
whiteSpace: "nowrap",
maxWidth: "100%",
overflow: "hidden",
textOverflow: "ellipsis",
},
serversResume: {
marginBottom: 15,
},
objectGeneralTitle: {
fontWeight: "bold",
color: "#000",
display: "flex",
alignItems: "center",
"& svg": {
width: 14,
},
},
generalContainer: {
padding: 20,
},
generalUnit: {
color: "#000",
marginTop: 6,
fontSize: 12,
fontWeight: "bold",
},
testUnitRes: {
fontSize: 120,
color: "#081C42",
fontWeight: "bold",
},
shareResults: {
padding: "18px 25px",
color: "#07193E",
fontSize: 14,
},
metricValContainer: {
lineHeight: 1,
},
actionButtons: {
textAlign: "right",
},
descriptorLabel: {
fontWeight: "bold",
fontSize: 14,
},
resultsContainer: {
backgroundColor: "#FBFAFA",
borderTop: "#F1F1F1 1px solid",
marginTop: 30,
padding: 25,
},
resultsIcon: {
display: "flex",
alignItems: "center",
"& svg": {
fill: "#07193E",
},
},
detailedItem: {
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
},
detailedVersion: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
},
serversTable: {
width: "100%",
marginTop: 15,
"& thead > tr > th": {
textAlign: "left",
padding: 15,
fontSize: 14,
fontWeight: "bold",
},
"& tbody > tr": {
"&:last-of-type": {
"& > td": {
borderBottom: "#E2E2E2 1px solid",
},
},
"& > td": {
borderTop: "#E2E2E2 1px solid",
padding: 15,
fontSize: 14,
"&:first-of-type": {
borderLeft: "#E2E2E2 1px solid",
},
"&:last-of-type": {
borderRight: "#E2E2E2 1px solid",
},
},
},
},
serverIcon: {
width: 55,
},
serverUnit: {
width: 70,
},
serverValue: {
width: 70,
},
serverHost: {
maxWidth: 540,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
tableOverflow: {
overflowX: "auto",
paddingBottom: 15,
},
objectGeneral: {
marginTop: 15,
},
});
const STResults = ({ classes, results }: ISTResults) => {
const [jsonView, setJsonView] = useState<boolean>(false);
const finalRes = results[results.length - 1];
const getServers: STServer[] = get(finalRes, "GETStats.servers", []) || [];
const putServers: STServer[] = get(finalRes, "PUTStats.servers", []) || [];
const getThroughput = get(finalRes, "GETStats.throughputPerSec", 0);
const getObjects = get(finalRes, "GETStats.objectsPerSec", 0);
const putThroughput = get(finalRes, "PUTStats.throughputPerSec", 0);
const putObjects = get(finalRes, "PUTStats.objectsPerSec", 0);
const ObjectGeneral = ({
title,
throughput,
objects,
}: {
title: any;
throughput: string;
objects: number;
}) => {
const avg = calculateBytes(throughput);
return (
<Grid container>
<Grid item xs={12} className={classes.objectGeneralTitle}>
{title}
</Grid>
<Grid item xs={12} md={6} className={classes.metricValContainer}>
<span className={classes.testUnitRes}>{avg.total}</span>
<span className={classes.generalUnit}>{avg.unit}/S</span>
</Grid>
</Grid>
);
};
let statJoin: IndvServerMetric[] = [];
getServers.forEach((item) => {
const hostName = item.endpoint;
const putMetric = putServers.find((item) => item.endpoint === hostName);
let itemJoin: IndvServerMetric = {
getUnit: "-",
getValue: "N/A",
host: item.endpoint,
putUnit: "-",
putValue: "N/A",
};
if (item.err && item.err !== "") {
itemJoin.getError = item.err;
itemJoin.getUnit = "-";
itemJoin.getValue = "N/A";
} else {
const niceGet = calculateBytes(item.throughputPerSec.toString());
itemJoin.getUnit = niceGet.unit;
itemJoin.getValue = niceGet.total.toString();
}
if (putMetric) {
if (putMetric.err && putMetric.err !== "") {
itemJoin.putError = putMetric.err;
itemJoin.putUnit = "-";
itemJoin.putValue = "N/A";
} else {
const nicePut = calculateBytes(putMetric.throughputPerSec.toString());
itemJoin.putUnit = nicePut.unit;
itemJoin.putValue = nicePut.total.toString();
}
}
statJoin.push(itemJoin);
});
const downloadResults = () => {
const date = new Date();
let element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + JSON.stringify(finalRes)
);
element.setAttribute(
"download",
`speedtest_results-${date.toISOString()}.log`
);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
const toggleJSONView = () => {
setJsonView(!jsonView);
};
const finalResJSON = finalRes ? JSON.stringify(finalRes, null, 4) : "";
return (
<Fragment>
<Grid container className={classes.objectGeneral}>
<Grid item xs={12} md={6} lg={4}>
<ObjectGeneral
title={
<Fragment>
<DownloadStatIcon />
&nbsp; GET
</Fragment>
}
throughput={getThroughput}
objects={getObjects}
/>
</Grid>
<Grid item xs={12} md={6} lg={4}>
<ObjectGeneral
title={
<Fragment>
<UploadStatIcon />
&nbsp; PUT
</Fragment>
}
throughput={putThroughput}
objects={putObjects}
/>
</Grid>
<Grid item xs={12} md={12} lg={4}>
<ResponsiveContainer width="99%">
<AreaChart data={cleanMetrics(results)}>
<defs>
<linearGradient id="colorPut" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#2781B0" stopOpacity={0.9} />
<stop offset="95%" stopColor="#fff" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorGet" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#4CCB92" stopOpacity={0.9} />
<stop offset="95%" stopColor="#fff" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray={"0 0"}
strokeWidth={1}
strokeOpacity={0.5}
stroke={"#F1F1F1"}
vertical={false}
/>
<Area
type="monotone"
dataKey={"get"}
stroke={"#4CCB92"}
fill={"url(#colorGet)"}
fillOpacity={0.3}
strokeWidth={2}
dot={false}
/>
<Area
type="monotone"
dataKey={"put"}
stroke={"#2781B0"}
fill={"url(#colorPut)"}
fillOpacity={0.3}
strokeWidth={2}
dot={false}
/>
</AreaChart>
</ResponsiveContainer>
</Grid>
</Grid>
<br />
<Grid container>
<Grid item xs={12} md={6} className={classes.descriptorLabel}>
{jsonView ? "JSON Results:" : "Detailed Results:"}
</Grid>
<Grid item xs={12} md={6} className={classes.actionButtons}>
<BoxIconButton
aria-label="Download"
onClick={downloadResults}
size="large"
>
<DownloadIcon />
</BoxIconButton>
&nbsp;
<BoxIconButton
aria-label="Download"
onClick={toggleJSONView}
size="large"
>
<JSONIcon />
</BoxIconButton>
</Grid>
</Grid>
<Grid container className={classes.resultsContainer}>
{jsonView ? (
<Fragment>
<CodeMirrorWrapper
value={finalResJSON}
readOnly
onBeforeChange={() => {}}
/>
</Fragment>
) : (
<Fragment>
<Grid
item
xs={12}
sm={12}
md={1}
lg={1}
className={classes.resultsIcon}
alignItems={"flex-end"}
>
<ComputerLineIcon width={45} />
</Grid>
<Grid
item
xs={12}
sm={6}
md={3}
lg={2}
className={classes.detailedItem}
>
Nodes:&nbsp;<strong>{finalRes.servers}</strong>
</Grid>
<Grid
item
xs={12}
sm={6}
md={3}
lg={2}
className={classes.detailedItem}
>
Drives:&nbsp;<strong>{finalRes.disks}</strong>
</Grid>
<Grid
item
xs={12}
sm={6}
md={3}
lg={2}
className={classes.detailedItem}
>
Concurrent:&nbsp;<strong>{finalRes.concurrent}</strong>
</Grid>
<Grid
item
xs={12}
sm={12}
md={12}
lg={5}
className={classes.detailedVersion}
>
<span className={classes.versionIcon}>
<VersionIcon />
</span>{" "}
MinIO VERSION&nbsp;<strong>{finalRes.version}</strong>
</Grid>
<Grid item xs={12} className={classes.tableOverflow}>
<table
className={classes.serversTable}
cellSpacing={0}
cellPadding={0}
>
<thead>
<tr>
<th colSpan={2}>Servers</th>
<th colSpan={2}>GET</th>
<th colSpan={2}>PUT</th>
</tr>
</thead>
<tbody>
{statJoin.map((stats, index) => (
<tr key={`storage-${index.toString()}`}>
<td className={classes.serverIcon}>
<StorageIcon />
</td>
<td className={classes.serverHost}>{stats.host}</td>
{stats.getError && stats.getError !== "" ? (
<td colSpan={2}>{stats.getError}</td>
) : (
<Fragment>
<td className={classes.serverValue}>
{prettyNumber(parseFloat(stats.getValue))}
</td>
<td className={classes.serverUnit}>
{stats.getUnit}/s.
</td>
</Fragment>
)}
{stats.putError && stats.putError !== "" ? (
<td colSpan={2}>{stats.putError}</td>
) : (
<Fragment>
<td className={classes.serverValue}>
{prettyNumber(parseFloat(stats.putValue))}
</td>
<td className={classes.serverUnit}>
{stats.putUnit}/s.
</td>
</Fragment>
)}
</tr>
))}
</tbody>
</table>
</Grid>
</Fragment>
)}
</Grid>
</Fragment>
);
};
export default withStyles(styles)(STResults);

View File

@@ -0,0 +1,373 @@
// 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, useEffect, useState } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Button, CircularProgress, Grid } from "@mui/material";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import moment from "moment/moment";
import PageHeader from "../Common/PageHeader/PageHeader";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import { wsProtocol } from "../../../utils/wsUtils";
import { SpeedTestResponse } from "./types";
import STResults from "./STResults";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import BackLink from "../../../common/BackLink";
import ProgressBarWrapper from "../Common/ProgressBarWrapper/ProgressBarWrapper";
import InputUnitMenu from "../Common/FormComponents/InputUnitMenu/InputUnitMenu";
interface ISpeedtest {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
boxy: {
border: "#E5E5E5 1px solid",
borderRadius: 2,
padding: 40,
backgroundColor: "#fff",
},
advancedConfiguration: {
color: "#2781B0",
fontSize: 10,
textDecoration: "underline",
border: "none",
backgroundColor: "transparent",
cursor: "pointer",
alignItems: "center",
display: "flex",
float: "right",
"&:hover": {
color: "#07193E",
},
"& svg": {
width: 10,
alignSelf: "center",
marginLeft: 5,
},
},
advancedOpen: {
transform: "rotateZ(-90deg) translateX(-4px) translateY(2px)",
},
advancedClosed: {
transform: "rotateZ(90deg)",
},
advancedContent: {
backgroundColor: "#FBFAFA",
maxHeight: 0,
transitionDuration: "0.3s",
overflow: "hidden",
padding: "0 15px",
marginTop: 15,
justifyContent: "space-between",
"&.open": {
maxHeight: 400,
},
},
advancedButton: {
flexGrow: 1,
alignItems: "flex-end",
display: "flex",
justifyContent: "flex-end",
},
progressContainer: {
padding: "0 15px",
},
stepProgressText: {
fontSize: 13,
marginBottom: 8,
},
advancedOption: {
marginTop: 20,
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});
const Speedtest = ({ classes }: ISpeedtest) => {
const [start, setStart] = useState<boolean>(false);
const [currStatus, setCurrStatus] = useState<SpeedTestResponse[] | null>(
null
);
const [duration, setDuration] = useState<string>("10");
const [durationUnit, setDurationUnit] = useState<string>("s");
const [size, setSize] = useState<string>("64");
const [sizeUnit, setSizeUnit] = useState<string>("MB");
const [concurrent, setConcurrent] = useState<string>("");
const [topDate, setTopDate] = useState<number>(0);
const [currentValue, setCurrentValue] = useState<number>(0);
const [totalSeconds, setTotalSeconds] = useState<number>(0);
const [speedometerValue, setSpeedometerValue] = useState<number>(0);
const [advancedOpen, setAdvancedOpen] = useState<boolean>(false);
useEffect(() => {
// begin watch if bucketName in bucketList and start pressed
if (start) {
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket(
`${wsProt}://${
url.hostname
}:${port}/ws/speedtest?duration=${duration}${durationUnit}&size=${size}${sizeUnit}${
concurrent.trim() !== "" ? `&concurrent=${concurrent}` : ""
}`
);
const baseDate = moment();
const currentTime = baseDate.unix() / 1000;
const incrementDate =
baseDate
.add(
parseInt(duration) * 2,
durationUnit as moment.unitOfTime.DurationConstructor
)
.unix() / 1000;
const totalSeconds = (incrementDate - currentTime) / 1000;
setTopDate(incrementDate);
setCurrentValue(currentTime);
setTotalSeconds(totalSeconds);
let interval: any | null = null;
if (c !== null) {
c.onopen = () => {
console.log("WebSocket Client Connected");
c.send("ok");
interval = setInterval(() => {
c.send("ok");
}, 10 * 1000);
};
c.onmessage = (message: IMessageEvent) => {
const data: SpeedTestResponse = JSON.parse(message.data.toString());
setCurrStatus((prevStatus) => {
let prSt: SpeedTestResponse[] = [];
if (prevStatus) {
prSt = [...prevStatus];
}
return [...prSt, data];
});
const currTime = moment().unix() / 1000;
setCurrentValue(currTime);
};
c.onclose = () => {
clearInterval(interval);
console.log("connection closed by server");
// reset start status
setStart(false);
};
return () => {
// close websocket on useEffect cleanup
c.close(1000);
clearInterval(interval);
console.log("closing websockets");
};
}
} else {
// reset start status
setStart(false);
}
}, [concurrent, duration, durationUnit, size, sizeUnit, start]);
useEffect(() => {
const actualSeconds = (topDate - currentValue) / 1000;
let percToDisplay = 100 - (actualSeconds * 100) / totalSeconds;
if (percToDisplay > 100) {
percToDisplay = 100;
}
setSpeedometerValue(percToDisplay);
}, [start, currentValue, topDate, totalSeconds]);
return (
<Fragment>
<PageHeader label="Speedtest" />
<Grid container className={classes.container}>
<Grid item xs={12}>
<BackLink to="/tools" label="Return to Tools" />
</Grid>
<Grid item xs={12} className={classes.boxy}>
<Grid container>
<Grid item>
<Button
onClick={() => {
setCurrStatus(null);
setStart(true);
}}
color="primary"
type="button"
variant={
currStatus !== null && !start ? "contained" : "outlined"
}
className={`${classes.buttonBackground} ${classes.speedStart}`}
disabled={duration.trim() === "" || size.trim() === "" || start}
>
{!start && (
<Fragment>
{currStatus !== null ? "Retest" : "Start"}
</Fragment>
)}
{start ? "Start" : ""}
</Button>
</Grid>
<Grid item md={9} sm={12} className={classes.progressContainer}>
<div className={classes.stepProgressText}>
{start ? (
"Speedtest in progress..."
) : (
<Fragment>
{currStatus && !start ? "Done!" : "Start a new test"}
</Fragment>
)}
&nbsp;&nbsp;&nbsp;{start && <CircularProgress size={15} />}
</div>
<div>
<ProgressBarWrapper
value={speedometerValue}
ready={currStatus !== null && !start}
/>
</div>
</Grid>
<Grid item className={classes.advancedButton}>
<button
onClick={() => {
setAdvancedOpen(!advancedOpen);
}}
className={classes.advancedConfiguration}
>
{advancedOpen ? "Hide" : "Show"} advanced options{" "}
<span
className={
advancedOpen ? classes.advancedOpen : classes.advancedClosed
}
>
<ArrowForwardIosIcon />
</span>
</button>
</Grid>
</Grid>
<Grid
container
className={`${classes.advancedContent} ${
advancedOpen ? "open" : ""
}`}
>
<Grid item xs={12} md={3} className={classes.advancedOption}>
<InputBoxWrapper
id={"duration"}
name={"duration"}
label={"Duration"}
onChange={(e) => {
setDuration(e.target.value);
}}
value={duration}
disabled={start}
overlayObject={
<InputUnitMenu
id={"duration-unit"}
onUnitChange={setDurationUnit}
unitSelected={durationUnit}
unitsList={[
{ label: "miliseconds", value: "ms" },
{ label: "seconds", value: "s" },
]}
disabled={start}
/>
}
/>
</Grid>
<Grid item xs={12} md={3} className={classes.advancedOption}>
<InputBoxWrapper
id={"size"}
name={"size"}
label={"Object Size"}
onChange={(e) => {
setSize(e.target.value);
}}
value={size}
disabled={start}
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={setSizeUnit}
unitSelected={sizeUnit}
unitsList={[
{ label: "KB", value: "KB" },
{ label: "MB", value: "MB" },
{ label: "GB", value: "GB" },
]}
disabled={start}
/>
}
/>
</Grid>
<Grid item xs={12} md={3} className={classes.advancedOption}>
<InputBoxWrapper
type="number"
min="0"
id={"concurrent"}
name={"concurrent"}
label={"Concurrent Requests"}
onChange={(e) => {
setConcurrent(e.target.value);
}}
value={concurrent}
disabled={start}
/>
</Grid>
</Grid>
<Grid container className={classes.multiModule}>
<Grid item xs={12}>
<Fragment>
<Grid item xs={12}>
{!start && currStatus !== null && (
<Fragment>
<STResults results={currStatus} />
</Fragment>
)}
</Grid>
</Fragment>
</Grid>
</Grid>
</Grid>
</Grid>
</Fragment>
);
};
export default withStyles(styles)(Speedtest);

View File

@@ -0,0 +1,48 @@
// 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/>.
export interface SpeedTestResponse {
version: string;
servers: number;
disks: number;
size: number;
concurrent: number;
PUTStats?: STStats;
GETStats?: STStats;
}
export interface STStats {
throughputPerSec: number;
objectsPerSec: number;
servers: STServer[] | null;
}
export interface STServer {
endpoint: string;
throughputPerSec: number;
objectsPerSec: number;
err: string;
}
export interface IndvServerMetric {
host: string;
getValue: string;
getUnit: string;
getError?: string;
putValue: string;
putUnit: string;
putError?: string;
}

View File

@@ -0,0 +1,32 @@
// 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 { SpeedTestResponse } from "./types";
export const cleanMetrics = (results: SpeedTestResponse[]) => {
const cleanRes = results.filter(
(item) => item.version !== "0" && item.disks !== 0
);
const states = cleanRes.map((itemRes) => {
return {
get: itemRes.GETStats?.throughputPerSec || 0,
put: itemRes.PUTStats?.throughputPerSec || 0,
};
});
return [{ get: 0, put: 0 }, ...states];
};

View File

@@ -42,8 +42,8 @@ import SearchIcon from "../../../../icons/SearchIcon";
import PageHeader from "../../Common/PageHeader/PageHeader";
import TenantListItem from "./TenantListItem";
import HelpBox from "../../../../common/HelpBox";
import BoxIconButton from "../../Common/BoxIconButton";
import AButton from "../../Common/AButton";
import BoxIconButton from "../../Common/BoxIconButton/BoxIconButton";
import AButton from "../../Common/AButton/AButton";
interface ITenantsList {
classes: any;

View File

@@ -58,7 +58,7 @@ import ScreenTitle from "../../Common/ScreenTitle/ScreenTitle";
import EditIcon from "../../../../icons/EditIcon";
import RefreshIcon from "../../../../icons/RefreshIcon";
import TenantsIcon from "../../../../icons/TenantsIcon";
import BoxIconButton from "../../Common/BoxIconButton";
import BoxIconButton from "../../Common/BoxIconButton/BoxIconButton";
interface ITenantDetailsProps {
classes: any;

View File

@@ -35,7 +35,7 @@ import { AppState } from "../../../../store";
import history from "./../../../../history";
import { CircleIcon } from "../../../../icons";
import { tenantIsOnline } from "../ListTenants/utils";
import AButton from "../../Common/AButton";
import AButton from "../../Common/AButton/AButton";
interface ITenantsSummary {
classes: any;

View File

@@ -23,6 +23,7 @@ import {
TraceIcon,
WatchIcon,
} from "../../../icons";
import SpeedtestIcon from "../../../icons/SpeedtestIcon";
export const configurationElements: IElement[] = [
{
@@ -55,4 +56,9 @@ export const configurationElements: IElement[] = [
configuration_id: "diagnostics",
configuration_label: "Diagnostics",
},
{
icon: <SpeedtestIcon />,
configuration_id: "speedtest",
configuration_label: "Speedtest",
},
];

View File

@@ -47,7 +47,7 @@ import PageHeader from "../Common/PageHeader/PageHeader";
import SearchIcon from "../../../icons/SearchIcon";
import { decodeFileName } from "../../../common/utils";
import HelpBox from "../../../common/HelpBox";
import AButton from "../Common/AButton";
import AButton from "../Common/AButton/AButton";
const styles = (theme: Theme) =>
createStyles({

View File

@@ -54,8 +54,8 @@ import ListItemText from "@mui/material/ListItemText";
import List from "@mui/material/List";
import LockIcon from "@mui/icons-material/Lock";
import ScreenTitle from "../Common/ScreenTitle/ScreenTitle";
import BoxIconButton from "../Common/BoxIconButton";
import PanelTitle from "../Common/PanelTitle";
import BoxIconButton from "../Common/BoxIconButton/BoxIconButton";
import PanelTitle from "../Common/PanelTitle/PanelTitle";
const styles = (theme: Theme) =>
createStyles({

View File

@@ -35,7 +35,7 @@ import DeleteServiceAccount from "../Account/DeleteServiceAccount";
import CredentialsPrompt from "../Common/CredentialsPrompt/CredentialsPrompt";
import { AddIcon } from "../../../icons";
import Button from "@mui/material/Button";
import PanelTitle from "../Common/PanelTitle";
import PanelTitle from "../Common/PanelTitle/PanelTitle";
interface IUserServiceAccountsProps {
classes: any;

View File

@@ -40,7 +40,7 @@ import (
func registerAdminInfoHandlers(api *operations.ConsoleAPI) {
// return usage stats
api.AdminAPIAdminInfoHandler = admin_api.AdminInfoHandlerFunc(func(params admin_api.AdminInfoParams, session *models.Principal) middleware.Responder {
infoResp, err := getAdminInfoResponse(session)
infoResp, err := getAdminInfoResponse(session, params)
if err != nil {
return admin_api.NewAdminInfoDefault(int(err.Code)).WithPayload(err)
}
@@ -824,8 +824,13 @@ type LabelResults struct {
}
// getAdminInfoResponse returns the response containing total buckets, objects and usage.
func getAdminInfoResponse(session *models.Principal) (*models.AdminInfoResponse, *models.Error) {
prometheusURL := getPrometheusURL()
func getAdminInfoResponse(session *models.Principal, params admin_api.AdminInfoParams) (*models.AdminInfoResponse, *models.Error) {
prometheusURL := ""
if !*params.DefaultOnly {
prometheusURL = getPrometheusURL()
}
mAdmin, err := NewMinioAdminClient(session)
if err != nil {
return nil, prepareError(err)

119
restapi/admin_speedtest.go Normal file
View File

@@ -0,0 +1,119 @@
// 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/>.
package restapi
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"github.com/dustin/go-humanize"
"github.com/gorilla/websocket"
"github.com/minio/madmin-go"
)
// getSpeedtesthOptionsFromReq gets duration, size & concurrent requests from a websocket
// path come as : `/speedtest?duration=2h&size=12MiB&concurrent=10`
func getSpeedtestOptionsFromReq(req *http.Request) (*madmin.SpeedtestOpts, error) {
optionsSet := madmin.SpeedtestOpts{}
queryPairs := req.URL.Query()
paramDuration := queryPairs.Get("duration")
if paramDuration == "" {
paramDuration = "10s"
}
duration, err := time.ParseDuration(paramDuration)
if err != nil {
return nil, fmt.Errorf("unable to parse duration: %s", paramDuration)
}
if duration <= 0 {
return nil, fmt.Errorf("duration cannot be 0 or negative")
}
optionsSet.Duration = duration
paramSize := queryPairs.Get("size")
if paramSize == "" {
paramSize = "64MiB"
}
size, err := humanize.ParseBytes(paramSize)
if err != nil {
return nil, fmt.Errorf("unable to parse object size")
}
if size < 0 {
return nil, fmt.Errorf("size is expected to be atleast 0 bytes")
}
optionsSet.Size = int(size)
paramConcurrent := queryPairs.Get("concurrent")
if paramConcurrent == "" {
paramConcurrent = "32"
}
concurrent, err := strconv.Atoi(paramConcurrent)
if err != nil {
return nil, fmt.Errorf("invalid concurrent value: %s", paramConcurrent)
}
if concurrent <= 0 {
return nil, fmt.Errorf("concurrency cannot be '0' or negative")
}
optionsSet.Concurrency = concurrent
return &optionsSet, nil
}
func startSpeedtest(ctx context.Context, conn WSConn, client MinioAdmin, speedtestOpts *madmin.SpeedtestOpts) error {
speedtestRes, err := client.speedtest(ctx, *speedtestOpts)
if err != nil {
LogError("error initializing speedtest: %v", err)
return err
}
for result := range speedtestRes {
// Serializing message
bytes, err := json.Marshal(result)
if err != nil {
LogError("error serializing json: %v", err)
return err
}
// Send Message through websocket connection
err = conn.writeMessage(websocket.TextMessage, bytes)
if err != nil {
LogError("error writing speedtest response: %v", err)
return err
}
}
return nil
}

View File

@@ -51,3 +51,7 @@ func (ac adminClientMock) addRemoteBucket(ctx context.Context, bucket string, ta
func (ac adminClientMock) changePassword(ctx context.Context, accessKey, secretKey string) error {
return minioChangePasswordMock(ctx, accessKey, secretKey)
}
func (ac adminClientMock) speedtest(ctx context.Context, opts madmin.SpeedtestOpts) (chan madmin.SpeedTestResult, error) {
return nil, nil
}

View File

@@ -112,6 +112,8 @@ type MinioAdmin interface {
addTier(ctx context.Context, tier *madmin.TierConfig) error
// Edit Tier Credentials
editTierCreds(ctx context.Context, tierName string, creds madmin.TierCreds) error
// Speedtest
speedtest(ctx context.Context, opts madmin.SpeedtestOpts) (chan madmin.SpeedTestResult, error)
}
// Interface implementation
@@ -452,3 +454,7 @@ func GetConsoleHTTPClient() *http.Client {
}
return httpClient
}
func (ac AdminClient) speedtest(ctx context.Context, opts madmin.SpeedtestOpts) (chan madmin.SpeedTestResult, error) {
return ac.Client.Speedtest(ctx, opts)
}

View File

@@ -142,6 +142,14 @@ func init() {
],
"summary": "Returns information about the deployment",
"operationId": "AdminInfo",
"parameters": [
{
"type": "boolean",
"default": false,
"name": "defaultOnly",
"in": "query"
}
],
"responses": {
"200": {
"description": "A successful response.",
@@ -5789,6 +5797,14 @@ func init() {
],
"summary": "Returns information about the deployment",
"operationId": "AdminInfo",
"parameters": [
{
"type": "boolean",
"default": false,
"name": "defaultOnly",
"in": "query"
}
],
"responses": {
"200": {
"description": "A successful response.",

View File

@@ -26,15 +26,25 @@ import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// NewAdminInfoParams creates a new AdminInfoParams object
//
// There are no default values defined in the spec.
// with the default values initialized.
func NewAdminInfoParams() AdminInfoParams {
return AdminInfoParams{}
var (
// initialize parameters with default values
defaultOnlyDefault = bool(false)
)
return AdminInfoParams{
DefaultOnly: &defaultOnlyDefault,
}
}
// AdminInfoParams contains all the bound params for the admin info operation
@@ -45,6 +55,12 @@ type AdminInfoParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
In: query
Default: false
*/
DefaultOnly *bool
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
@@ -56,8 +72,38 @@ func (o *AdminInfoParams) BindRequest(r *http.Request, route *middleware.Matched
o.HTTPRequest = r
qs := runtime.Values(r.URL.Query())
qDefaultOnly, qhkDefaultOnly, _ := qs.GetOK("defaultOnly")
if err := o.bindDefaultOnly(qDefaultOnly, qhkDefaultOnly, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindDefaultOnly binds and validates parameter DefaultOnly from query.
func (o *AdminInfoParams) bindDefaultOnly(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
// Default values have been previously initialized by NewAdminInfoParams()
return nil
}
value, err := swag.ConvertBool(raw)
if err != nil {
return errors.InvalidType("defaultOnly", "query", "bool", raw)
}
o.DefaultOnly = &value
return nil
}

View File

@@ -26,11 +26,17 @@ import (
"errors"
"net/url"
golangswaggerpaths "path"
"github.com/go-openapi/swag"
)
// AdminInfoURL generates an URL for the admin info operation
type AdminInfoURL struct {
DefaultOnly *bool
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
@@ -60,6 +66,18 @@ func (o *AdminInfoURL) Build() (*url.URL, error) {
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
qs := make(url.Values)
var defaultOnlyQ string
if o.DefaultOnly != nil {
defaultOnlyQ = swag.FormatBool(*o.DefaultOnly)
}
if defaultOnlyQ != "" {
qs.Set("defaultOnly", defaultOnlyQ)
}
_result.RawQuery = qs.Encode()
return &_result, nil
}

View File

@@ -18,6 +18,7 @@ package restapi
import (
"context"
"fmt"
"net"
"net/http"
"strconv"
@@ -28,6 +29,7 @@ import (
"github.com/gorilla/websocket"
"github.com/minio/console/models"
"github.com/minio/console/pkg/auth"
"github.com/minio/madmin-go"
)
var upgrader = websocket.Upgrader{
@@ -212,6 +214,23 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
return
}
go wsS3Client.watch(wOptions)
case strings.HasPrefix(wsPath, `/speedtest`):
fmt.Println("Speedtest triggered")
speedtestOpts, err := getSpeedtestOptionsFromReq(req)
if err != nil {
LogError("error getting speedtest options: %v", err)
closeWsConn(conn)
return
}
wsAdminClient, err := newWebSocketAdminClient(conn, session)
if err != nil {
closeWsConn(conn)
return
}
go wsAdminClient.speedtest(speedtestOpts)
default:
// path not found
closeWsConn(conn)
@@ -374,6 +393,21 @@ func (wsc *wsAdminClient) healthInfo(deadline *time.Duration) {
sendWsCloseMessage(wsc.conn, err)
}
func (wsc *wsAdminClient) speedtest(opts *madmin.SpeedtestOpts) {
defer func() {
LogInfo("speedtest stopped")
// close connection after return
wsc.conn.close()
}()
LogInfo("speedtest started")
ctx := wsReadClientCtx(wsc.conn)
err := startSpeedtest(ctx, wsc.conn, wsc.client, opts)
sendWsCloseMessage(wsc.conn, err)
}
// sendWsCloseMessage sends Websocket Connection Close Message indicating the Status Code
// see https://tools.ietf.org/html/rfc6455#page-45
func sendWsCloseMessage(conn WSConn, err error) {

View File

@@ -1981,6 +1981,12 @@ paths:
get:
summary: Returns information about the deployment
operationId: AdminInfo
parameters:
- name: defaultOnly
in: query
required: false
type: boolean
default: false
responses:
200:
description: A successful response.