Add get healthInfo api using websockets (#543)

Integrate also HealthInfo API with Console UI
This commit is contained in:
Cesar N
2021-01-13 14:43:34 -06:00
committed by GitHub
parent 1c109769df
commit d6aceb5430
32 changed files with 1814 additions and 399 deletions

View File

@@ -50,7 +50,7 @@ swagger-gen:
@swagger generate server -A console --main-package=console --exclude-main -P models.Principal -f ./swagger.yml -r NOTICE
assets:
@(cd portal-ui; yarn install; make build-static; cd ..)
@(cd portal-ui; yarn install; make build-static; yarn prettier --write . --loglevel warn; cd ..)
test:
@(GO111MODULE=on go test -race -v github.com/minio/console/restapi/...)

4
go.sum
View File

@@ -319,6 +319,7 @@ github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
@@ -602,6 +603,7 @@ github.com/goreleaser/nfpm v1.2.1/go.mod h1:TtWrABZozuLOttX2uDlYyECfQX7x5XYkVxhj
github.com/goreleaser/nfpm v1.3.0/go.mod h1:w0p7Kc9TAUgWMyrub63ex3M2Mgw88M4GZXoTq5UCb40=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@@ -922,6 +924,7 @@ github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88J
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.35 h1:oTfOaDH+mZkdcgdIjH6yBajRGtIwcwcaR+rt23ZSrJs=
github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/minio/cli v1.22.0 h1:VTQm7lmXm3quxO917X3p+el1l0Ca5X3S4PM2ruUYO68=
github.com/minio/cli v1.22.0/go.mod h1:bYxnK0uS629N3Bq+AOZZ+6lwF77Sodk4+UL9vNuXhOY=
@@ -1346,6 +1349,7 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=

View File

@@ -43,6 +43,7 @@ var (
heal = "/heal"
trace = "/trace"
logs = "/logs"
healthInfo = "/health-info"
)
type ConfigurationActionSet struct {
@@ -231,6 +232,16 @@ var traceActionSet = ConfigurationActionSet{
),
}
// healthInfoActionSet contains the list of admin actions required for this endpoint to work
var healthInfoActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.HealthInfoAdminAction,
),
}
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet,
@@ -252,6 +263,7 @@ var endpointRules = map[string]ConfigurationActionSet{
heal: healActionSet,
trace: traceActionSet,
logs: logsActionSet,
healthInfo: healthInfoActionSet,
}
// operatorRules contains the mapping between endpoints and ActionSets for operator only mode

View File

@@ -72,7 +72,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:*",
},
},
want: 17,
want: 18,
},
{
name: "all s3 endpoints",
@@ -91,7 +91,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 19,
want: 20,
},
{
name: "Console User - default endpoints",

View File

@@ -0,0 +1,2 @@
build
coverage

View File

@@ -0,0 +1 @@
{}

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,10 @@
const rewireReactHotLoader = require('react-app-rewire-hot-loader');
const rewireReactHotLoader = require("react-app-rewire-hot-loader");
/* config-overrides.js */
module.exports = function override(config, env) {
if (env === 'development') {
config.resolve.alias['react-dom'] = '@hot-loader/react-dom';
}
config = rewireReactHotLoader(config, env);
return config;
if (env === "development") {
config.resolve.alias["react-dom"] = "@hot-loader/react-dom";
}
config = rewireReactHotLoader(config, env);
return config;
};

View File

@@ -79,7 +79,7 @@
"proxy": "http://localhost:9090/",
"devDependencies": {
"jest": "^24.9.0",
"prettier": "^1.19.1",
"prettier": "2.2.1",
"typescript": "^4.1.2"
}
}

View File

@@ -4,21 +4,44 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="MinIO Console"
/>
<meta name="description" content="MinIO Console" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;500;700;900&display=swap" rel="stylesheet">
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="%PUBLIC_URL%/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="mask-icon" href="%PUBLIC_URL%/safari-pinned-tab.svg" color="#3a4e54">
<link
href="https://fonts.googleapis.com/css2?family=Lato:wght@400;500;700;900&display=swap"
rel="stylesheet"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="%PUBLIC_URL%/apple-icon-180x180.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="96x96"
href="%PUBLIC_URL%/favicon-96x96.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/favicon-16x16.png"
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link
rel="mask-icon"
href="%PUBLIC_URL%/safari-pinned-tab.svg"
color="#3a4e54"
/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.

View File

@@ -22,6 +22,7 @@ import {
USER_LOGGED,
SET_LOADING_PROGRESS,
SET_SNACK_BAR_MESSAGE,
SET_SERVER_DIAG_STAT,
} from "./types";
export function userLoggedIn(loggedIn: boolean) {
@@ -72,3 +73,10 @@ export const setSnackBarMessage = (message: string) => {
snackBarMessage: message,
};
};
export const setServerDiagStat = (status: string) => {
return {
type: SET_SERVER_DIAG_STAT,
serverDiagnosticStatus: status,
};
};

View File

@@ -1,4 +1,3 @@
import { createBrowserHistory } from "history";
export default createBrowserHistory();

View File

@@ -1,11 +1,11 @@
body {
margin: 0;
font-family: 'Lato', sans-serif;
font-family: "Lato", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

View File

@@ -24,6 +24,7 @@ import {
USER_LOGGED,
SET_LOADING_PROGRESS,
SET_SNACK_BAR_MESSAGE,
SET_SERVER_DIAG_STAT,
} from "./types";
const initialState: SystemState = {
@@ -36,6 +37,7 @@ const initialState: SystemState = {
serverIsLoading: false,
loadingProgress: 100,
snackBarMessage: "",
serverDiagnosticStatus: "",
};
export function systemReducer(
@@ -79,6 +81,11 @@ export function systemReducer(
...state,
snackBarMessage: action.snackBarMessage,
};
case SET_SERVER_DIAG_STAT:
return {
...state,
serverDiagnosticStatus: action.serverDiagnosticStatus,
};
default:
return state;
}

View File

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

View File

@@ -52,6 +52,7 @@ import Trace from "./Trace/Trace";
import LogsMain from "./Logs/LogsMain";
import Heal from "./Heal/Heal";
import Watch from "./Watch/Watch";
import HealthInfo from "./HealthInfo/HealthInfo";
const drawerWidth = 245;
@@ -288,6 +289,10 @@ const Console = ({
component: LogsMain,
path: "/logs",
},
{
component: HealthInfo,
path: "/health-info",
},
{
component: ConfigurationMain,
path: "/settings",

View File

@@ -0,0 +1,253 @@
// 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, { useState, useEffect } from "react";
import {
IMessageEvent,
w3cwebsocket as W3CWebSocket,
ICloseEvent,
} from "websocket";
import { AppState } from "../../../store";
import { connect } from "react-redux";
import { healthInfoMessageReceived, healthInfoResetMessage } from "./actions";
import {
HealthInfoMessage,
DiagStatInProgress,
DiagStatSuccess,
DiagStatError,
} from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
wsProtocol,
WSCloseInternalServerErr,
WSClosePolicyViolation,
} from "../../../utils/wsUtils";
import {
actionsTray,
containerForHeader,
} from "../Common/FormComponents/common/styleLibrary";
import { Grid, Button } from "@material-ui/core";
import PageHeader from "../Common/PageHeader/PageHeader";
import { setSnackBarMessage, setServerDiagStat } from "../../../actions";
const styles = (theme: Theme) =>
createStyles({
logList: {
background: "#fff",
minHeight: 400,
height: "calc(100vh - 270px)",
overflow: "auto",
fontSize: 13,
padding: "25px 45px",
border: "1px solid #EAEDEE",
borderRadius: 4,
},
...actionsTray,
...containerForHeader(theme.spacing(4)),
});
const download = (filename: string, text: string) => {
let element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
);
element.setAttribute("download", filename);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
interface IHealthInfo {
classes: any;
healthInfoMessageReceived: typeof healthInfoMessageReceived;
healthInfoResetMessage: typeof healthInfoResetMessage;
message: HealthInfoMessage;
namespace: string;
tenant: string;
setSnackBarMessage: typeof setSnackBarMessage;
setServerDiagStat: typeof setServerDiagStat;
serverDiagnosticStatus: string;
}
const HealthInfo = ({
classes,
healthInfoMessageReceived,
healthInfoResetMessage,
message,
setSnackBarMessage,
setServerDiagStat,
serverDiagnosticStatus,
}: IHealthInfo) => {
// TODO: Diagnostic button states should be global so that they are not overwritten
const [startDiagnostic, setStartDiagnostic] = useState(false);
const [downloadDisabled, setDownloadDisabled] = useState(true);
useEffect(() => {
if (
serverDiagnosticStatus === DiagStatSuccess &&
message !== ({} as HealthInfoMessage)
) {
// Allow download of diagnostics file only when
// it succeded fetching all the results and info is not empty.
setDownloadDisabled(false);
setStartDiagnostic(false);
}
if (serverDiagnosticStatus === DiagStatInProgress) {
// Disable Start Diagnotic and Disable Download buttons
// if a Diagnosis is in progress.
setStartDiagnostic(false);
setDownloadDisabled(true);
}
}, [serverDiagnosticStatus, message]);
useEffect(() => {
if (startDiagnostic) {
healthInfoResetMessage();
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/health-info?deadline=1h`
);
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);
setSnackBarMessage(
"Diagnostic started. Please do not refresh page during diagnosis."
);
setServerDiagStat(DiagStatInProgress);
};
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) => {
console.log("error, closing websocket:", error.message);
c.close(1000);
clearInterval(interval);
setServerDiagStat(DiagStatError);
};
c.onclose = (event: ICloseEvent) => {
clearInterval(interval);
if (
event.code === WSCloseInternalServerErr ||
event.code === WSClosePolicyViolation
) {
// handle close with error
console.log("connection closed by server with code:", event.code);
setSnackBarMessage(
"An error occurred while getting Diagnostic file."
);
setServerDiagStat(DiagStatError);
} else {
console.log("connection closed by server");
setSnackBarMessage("Diagnostic file is ready to be downloaded.");
setServerDiagStat(DiagStatSuccess);
}
};
}
} else {
// reset start status
setStartDiagnostic(false);
}
}, [
healthInfoMessageReceived,
healthInfoResetMessage,
startDiagnostic,
setSnackBarMessage,
setServerDiagStat,
]);
const renderResponse = (data: HealthInfoMessage) => {
// render response in json format
let str = JSON.stringify(data, null, 2);
if (str === "{}") {
str = "";
}
return <pre>{str}</pre>;
};
return (
<React.Fragment>
<PageHeader label="Diagnostic" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid container justify="flex-end" spacing={2}>
<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>
<Button
type="submit"
variant="contained"
color="primary"
onClick={() => {
download("diagnostic.json", JSON.stringify(message, null, 2));
}}
disabled={downloadDisabled}
>
Download
</Button>
</Grid>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<div className={classes.logList}>
<pre>{renderResponse(message)}</pre>
</div>
</Grid>
</Grid>
</Grid>
</React.Fragment>
);
};
const mapState = (state: AppState) => ({
message: state.healthInfo.message,
serverDiagnosticStatus: state.system.serverDiagnosticStatus,
});
const connector = connect(mapState, {
healthInfoMessageReceived: healthInfoMessageReceived,
healthInfoResetMessage: healthInfoResetMessage,
setSnackBarMessage,
setServerDiagStat,
});
export default connector(withStyles(styles)(HealthInfo));

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 { HealthInfoMessage } from "./types";
export const HEALTH_INFO_MESSAGE_RECEIVED = "HEALTH_INFO_MESSAGE_RECEIVED";
export const HEALTH_INFO_RESET_MESSAGE = "HEALTH_INFO_RESET_MESSAGE";
interface HealthInfoMessageReceivedAction {
type: typeof HEALTH_INFO_MESSAGE_RECEIVED;
message: HealthInfoMessage;
}
interface HealthInfoResetMessagesAction {
type: typeof HEALTH_INFO_RESET_MESSAGE;
}
export type HealthInfoActionTypes =
| HealthInfoMessageReceivedAction
| HealthInfoResetMessagesAction;
export function healthInfoMessageReceived(message: HealthInfoMessage) {
return {
type: HEALTH_INFO_MESSAGE_RECEIVED,
message: message,
};
}
export function healthInfoResetMessage() {
return {
type: HEALTH_INFO_RESET_MESSAGE,
};
}

View File

@@ -0,0 +1,50 @@
// 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 {
HEALTH_INFO_MESSAGE_RECEIVED,
HEALTH_INFO_RESET_MESSAGE,
HealthInfoActionTypes,
} from "./actions";
import { HealthInfoMessage } from "./types";
export interface HealthInfoState {
message: HealthInfoMessage;
}
const initialState: HealthInfoState = {
message: {} as HealthInfoMessage,
};
export function healthInfoReducer(
state = initialState,
action: HealthInfoActionTypes
): HealthInfoState {
switch (action.type) {
case HEALTH_INFO_MESSAGE_RECEIVED:
return {
...state,
message: action.message,
};
case HEALTH_INFO_RESET_MESSAGE:
return {
...state,
message: {} as HealthInfoMessage,
};
default:
return state;
}
}

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/>.
export const DiagStatError = "error";
export const DiagStatSuccess = "success";
export const DiagStatInProgress = "inProgress";
export interface HealthInfoMessage {
timestamp: Date;
error: string;
perf: perfInfo;
minio: minioHealthInfo;
sys: sysHealthInfo;
}
export interface perfInfo {
drives: serverDrivesInfo[];
net: serverNetHealthInfo[];
net_parallel: serverNetHealthInfo;
error: string;
}
export interface serverDrivesInfo {
addr: string;
serial: drivePerfInfo[];
parallel: drivePerfInfo[];
error: string;
}
export interface drivePerfInfo {
endpoint: string;
latency: diskLatency;
throughput: diskThroughput;
error: string;
}
export interface diskLatency {
avg_secs: number;
percentile50_secs: number;
percentile90_secs: number;
percentile99_secs: number;
min_secs: number;
max_secs: number;
}
export interface diskThroughput {
avg_bytes_per_sec: number;
percentile50_bytes_per_sec: number;
percentile90_bytes_per_sec: number;
percentile99_bytes_per_sec: number;
min_bytes_per_sec: number;
max_bytes_per_sec: number;
}
export interface serverNetHealthInfo {
addr: string;
net: netPerfInfo[];
error: string;
}
export interface netPerfInfo {
remote: string;
latency: netLatency;
throughput: netThroughput;
error: string;
}
export interface netLatency {
avg_secs: number;
percentile50_secs: number;
percentile90_secs: number;
percentile99_secs: number;
min_secs: number;
max_secs: number;
}
export interface netThroughput {
avg_bytes_per_sec: number;
percentile50_bytes_per_sec: number;
percentile90_bytes_per_sec: number;
percentile99_bytes_per_sec: number;
min_bytes_per_sec: number;
max_bytes_per_sec: number;
}
export interface minioHealthInfo {
info: infoMessage;
config: any;
error: string;
}
export interface infoMessage {
mode: string;
domain: string[];
region: string;
sqsARN: string[];
deploymentID: string;
buckets: buckets;
objects: objects;
usage: usage;
services: services;
backend: any;
servers: serverProperties[];
}
export interface buckets {
count: number;
}
export interface objects {
count: number;
}
export interface usage {
size: number;
}
export interface services {
vault: vault;
ldap: ldap;
logger: Map<string, status[]>[];
audit: Map<string, status[]>[];
notifications: Map<string, Map<string, status[]>[]>;
}
export interface vault {
status: string;
encrypt: string;
decrypt: string;
}
export interface ldap {
status: string;
}
export interface status {
status: string;
}
export interface serverProperties {
state: string;
endpoint: string;
uptime: number;
version: string;
commitID: string;
network: Map<string, string>;
drives: disk[];
}
export interface disk {
endpoint: string;
rootDisk: boolean;
path: string;
healing: boolean;
state: string;
uuid: string;
model: string;
totalspace: number;
usedspace: number;
availspace: number;
readthroughput: number;
writethroughput: number;
readlatency: number;
writelatency: number;
utilization: number;
}
export interface sysHealthInfo {
cpus: serverCpuInfo[];
drives: serverDiskHwInfo[];
osinfos: serverOsInfo[];
meminfos: serverMemInfo[];
procinfos: serverProcInfo[];
error: string;
}
export interface serverCpuInfo {
addr: string;
cpu: cpuInfoStat[];
time: cpuTimeStat[];
error: string;
}
export interface cpuInfoStat {
cpu: number;
vendorId: string;
family: string;
model: string;
stepping: number;
physicalId: string;
coreId: string;
cores: number;
modelName: string;
mhz: number;
cacheSize: number;
flags: string[];
microcode: string;
}
export interface cpuTimeStat {
cpu: string;
user: number;
system: number;
idle: number;
nice: number;
iowait: number;
irq: number;
softirq: number;
steal: number;
guest: number;
guestNice: number;
}
export interface serverDiskHwInfo {
addr: string;
usages: diskUsageStat[];
partitions: partitionStat[];
counters: Map<string, diskIOCountersStat>;
error: string;
}
export interface diskUsageStat {
path: string;
fstype: string;
total: number;
free: number;
used: number;
usedPercent: number;
inodesTotal: number;
inodesUsed: number;
inodesFree: number;
inodesUsedPercent: number;
}
export interface partitionStat {
device: string;
mountpoint: string;
fstype: string;
opts: string;
smartInfo: smartInfo;
}
export interface smartInfo {
device: string;
scsi: scsiInfo;
nvme: nvmeInfo;
ata: ataInfo;
error: string;
}
export interface scsiInfo {
scsiCapacityBytes: number;
scsiModeSenseBuf: string;
scsirespLen: number;
scsiBdLen: number;
scsiOffset: number;
sciRpm: number;
}
export interface nvmeInfo {
serialNum: string;
vendorId: string;
firmwareVersion: string;
modelNum: string;
spareAvailable: string;
spareThreshold: string;
temperature: string;
criticalWarning: string;
maxDataTransferPages: number;
controllerBusyTime: number;
powerOnHours: number;
powerCycles: number;
unsafeShutdowns: number;
mediaAndDataIntgerityErrors: number;
dataUnitsReadBytes: number;
dataUnitsWrittenBytes: number;
hostReadCommands: number;
hostWriteCommands: number;
}
export interface ataInfo {
scsiLuWWNDeviceID: string;
serialNum: string;
modelNum: string;
firmwareRevision: string;
RotationRate: string;
MajorVersion: string;
MinorVersion: string;
smartSupportAvailable: boolean;
smartSupportEnabled: boolean;
smartErrorLog: string;
transport: string;
}
export interface diskIOCountersStat {
readCount: number;
mergedReadCount: number;
DriteCount: number;
mergedWriteCount: number;
readBytes: number;
writeBytes: number;
readTime: number;
writeTime: number;
iopsInProgress: number;
ioTime: number;
weightedIO: number;
name: string;
serialNumber: string;
label: string;
}
export interface serverOsInfo {
addr: string;
info: infoStat;
sensors: temperatureStat[];
users: userStat[];
error: string;
}
export interface infoStat {
hostname: string;
uptime: number;
bootTime: number;
procs: number;
os: string;
platform: string;
platformFamily: string;
platformVersion: string;
kernelVersion: string;
kernelArch: string;
virtualizationSystem: string;
virtualizationRole: string;
hostid: string;
}
export interface temperatureStat {
sensorKey: string;
sensorTemperature: number;
}
export interface userStat {
user: string;
terminal: string;
host: string;
started: number;
}
export interface serverMemInfo {
addr: string;
swap: swapMemoryStat;
virtualmem: virtualMemoryStat;
error: string;
}
export interface swapMemoryStat {
total: number;
used: number;
free: number;
usedPercent: number;
sin: number;
sout: number;
pgin: number;
pgout: number;
pgfault: number;
pgmajfault: number;
}
export interface virtualMemoryStat {
total: number;
available: number;
used: number;
usedPercent: number;
free: number;
active: number;
inactive: number;
wired: number;
laundry: number;
buffers: number;
cached: number;
writeback: number;
dirty: number;
writebacktmp: number;
shared: number;
slab: number;
sreclaimable: number;
sunreclaim: number;
pagetables: number;
swapcached: number;
commitlimit: number;
committedas: number;
hightotal: number;
highfree: number;
lowtotal: number;
lowfree: number;
swaptotal: number;
swapfree: number;
mapped: number;
vmalloctotal: number;
vmallocused: number;
vmallocchunk: number;
hugepagestotal: number;
hugepagesfree: number;
hugepagesize: number;
}
export interface serverProcInfo {
addr: string;
processes: sysProcess[];
error: string;
}
export interface sysProcess {
pid: number;
background: boolean;
cpupercent: number;
children: number[];
cmd: string;
connections: nethwConnectionStat[];
createtime: number;
cwd: string;
exe: string;
gids: number[];
iocounters: processIOCountersStat;
isrunning: boolean;
meminfo: memoryInfoStat;
memmaps: any[];
mempercent: number;
name: string;
netiocounters: nethwIOCounterStat[];
nice: number;
numctxswitches: processNmCtxSwitchesStat;
numfds: number;
numthreads: number;
pagefaults: processPageFaultsStat;
parent: number;
ppid: number;
rlimit: processRLimitStat[];
status: string;
tgid: number;
cputimes: cpuTimeStat;
uids: number[];
username: string;
}
export interface nethwConnectionStat {
fd: number;
family: number;
type: number;
localaddr: netAddr;
remoteaddr: netAddr;
status: string;
uids: number[];
pid: number;
}
export interface netAddr {
ip: string;
port: number;
}
export interface processIOCountersStat {
readCount: number;
writeCount: number;
readBytes: number;
writeBytes: number;
}
export interface memoryInfoStat {
rss: number;
vms: number;
hwm: number;
data: number;
stack: number;
locked: number;
swap: number;
}
export interface nethwIOCounterStat {
name: string;
bytesSent: number;
bytesRecv: number;
packetsSent: number;
packetsRecv: number;
errin: number;
errout: number;
dropin: number;
dropout: number;
fifoin: number;
fifoout: number;
}
export interface processNmCtxSwitchesStat {
voluntary: number;
involuntary: number;
}
export interface processPageFaultsStat {
minorFaults: number;
majorFaults: number;
childMinorFaults: number;
childMajorFaults: number;
}
export interface processRLimitStat {
resource: number;
soft: number;
hard: number;
used: number;
}

View File

@@ -52,6 +52,7 @@ import LogoutIcon from "../../../icons/LogoutIcon";
import ConsoleIcon from "../../../icons/ConsoleIcon";
import HealIcon from "../../../icons/HealIcon";
import WatchIcon from "../../../icons/WatchIcon";
import TrackChangesSharpIcon from "@material-ui/icons/TrackChangesSharp";
const styles = (theme: Theme) =>
createStyles({
@@ -275,6 +276,14 @@ const Menu = ({ userLoggedIn, classes, pages, operatorMode }: IMenuProps) => {
name: "Heal",
icon: <HealIcon />,
},
{
group: "Tools",
type: "item",
component: NavLink,
to: "/health-info",
name: "Diagnostic",
icon: <TrackChangesSharpIcon />,
},
{
group: "Admin",
type: "item",

View File

@@ -11,9 +11,9 @@
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
window.location.hostname === "[::1]" ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
@@ -26,7 +26,7 @@ type Config = {
};
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
@@ -39,7 +39,7 @@ export function register(config?: Config) {
return;
}
window.addEventListener('load', () => {
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
@@ -50,8 +50,8 @@ export function register(config?: Config) {
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
"This web app is being served cache-first by a service " +
"worker. To learn more, visit https://bit.ly/CRA-PWA"
);
});
} else {
@@ -65,21 +65,21 @@ export function register(config?: Config) {
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
"New content is available and will be used when all " +
"tabs for this page are closed. See https://bit.ly/CRA-PWA."
);
// Execute callback
@@ -90,7 +90,7 @@ function registerValidSW(swUrl: string, config?: Config) {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
console.log("Content is cached for offline use.");
// Execute callback
if (config && config.onSuccess) {
@@ -101,23 +101,23 @@ function registerValidSW(swUrl: string, config?: Config) {
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
.catch((error) => {
console.error("Error during service worker registration:", error);
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
const contentType = response.headers.get("content-type");
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
(contentType != null && contentType.indexOf("javascript") === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
@@ -129,14 +129,14 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
"No internet connection found. App is running in offline mode."
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}

View File

@@ -19,6 +19,7 @@ import thunk from "redux-thunk";
import { systemReducer } from "./reducer";
import { traceReducer } from "./screens/Console/Trace/reducers";
import { logReducer } from "./screens/Console/Logs/reducers";
import { healthInfoReducer } from "./screens/Console/HealthInfo/reducers";
import { watchReducer } from "./screens/Console/Watch/reducers";
import { consoleReducer } from "./screens/Console/reducer";
import { bucketsReducer } from "./screens/Console/Buckets/reducers";
@@ -32,6 +33,7 @@ const globalReducer = combineReducers({
console: consoleReducer,
buckets: bucketsReducer,
objectBrowser: objectBrowserReducer,
healthInfo: healthInfoReducer,
});
declare global {

View File

@@ -24,6 +24,7 @@ export interface SystemState {
serverIsLoading: boolean;
loadingProgress: number;
snackBarMessage: string;
serverDiagnosticStatus: string;
}
export const USER_LOGGED = "USER_LOGGED";
@@ -33,6 +34,7 @@ export const SERVER_NEEDS_RESTART = "SERVER_NEEDS_RESTART";
export const SERVER_IS_LOADING = "SERVER_IS_LOADING";
export const SET_LOADING_PROGRESS = "SET_LOADING_PROGRESS";
export const SET_SNACK_BAR_MESSAGE = "SET_SNACK_BAR_MESSAGE";
export const SET_SERVER_DIAG_STAT = "SET_SERVER_DIAG_STAT";
interface UserLoggedAction {
type: typeof USER_LOGGED;
@@ -68,6 +70,11 @@ interface SetSnackBarMessage {
snackBarMessage: string;
}
interface SetServerDiagStat {
type: typeof SET_SERVER_DIAG_STAT;
serverDiagnosticStatus: string;
}
export type SystemActionTypes =
| UserLoggedAction
| OperatorModeAction
@@ -75,4 +82,5 @@ export type SystemActionTypes =
| ServerNeedsRestartAction
| ServerIsLoading
| SetLoadingProgress
| SetSnackBarMessage;
| SetSnackBarMessage
| SetServerDiagStat;

View File

@@ -13,6 +13,13 @@
//
// 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/>.
// Close codes for websockets defined in RFC 6455
export const WSCloseNormalClosure = 1000;
export const WSCloseCloseGoingAway = 1001;
export const WSClosePolicyViolation = 1008;
export const WSCloseInternalServerErr = 1011;
export const wsProtocol = (protocol: string): string => {
let wsProtocol = "ws";
if (protocol === "https:") {

View File

@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es2015",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@@ -20,7 +16,5 @@
"jsx": "react-jsx",
"downlevelIteration": true
},
"include": [
"src"
]
"include": ["src"]
}

View File

@@ -8901,10 +8901,10 @@ prepend-http@^1.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
prettier@^1.19.1:
version "1.19.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
prettier@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
pretty-bytes@^5.1.0:
version "5.4.1"

View File

@@ -0,0 +1,94 @@
// 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"
"log"
"net/http"
"time"
"errors"
"github.com/gorilla/websocket"
madmin "github.com/minio/minio/pkg/madmin"
)
// startHealthInfo starts fetching mc.ServerHealthInfo and
// sends messages with the corresponding data on the websocket connection
func startHealthInfo(ctx context.Context, conn WSConn, client MinioAdmin, deadline *time.Duration) error {
if deadline == nil {
return errors.New("duration can't be nil on startHealthInfo")
}
// Fetch info of all servers (cluster or single server)
healthDataTypes := []madmin.HealthDataType{
madmin.HealthDataTypePerfDrive,
madmin.HealthDataTypePerfNet,
madmin.HealthDataTypeMinioInfo,
madmin.HealthDataTypeMinioConfig,
madmin.HealthDataTypeSysCPU,
madmin.HealthDataTypeSysDiskHw,
madmin.HealthDataTypeSysDocker,
madmin.HealthDataTypeSysOsInfo,
madmin.HealthDataTypeSysLoad,
madmin.HealthDataTypeSysMem,
madmin.HealthDataTypeSysNet,
madmin.HealthDataTypeSysProcess,
}
healthChan := client.serverHealthInfo(ctx, healthDataTypes, *deadline)
// wait for events to occur
for {
select {
// return if context ends
case <-ctx.Done():
return nil
case adminHealthInfo, ok := <-healthChan:
// zero value returned because the channel is closed and empty
if !ok {
return nil
}
if adminHealthInfo.Error != "" {
return errors.New(adminHealthInfo.Error)
}
// Serialize message to be sent
bytes, err := json.Marshal(adminHealthInfo)
if err != nil {
log.Println("error on json.Marshal:", err)
return err
}
// Send Message through websocket connection
err = conn.writeMessage(websocket.TextMessage, bytes)
if err != nil {
log.Println("error writeMessage:", err)
return err
}
}
}
}
// getHealthInfoOptionsFromReq gets duration for startHealthInfo request
// path come as : `/health-info?deadline=2h`
func getHealthInfoOptionsFromReq(req *http.Request) (*time.Duration, error) {
deadlineDuration, err := time.ParseDuration(req.FormValue("deadline"))
if err != nil {
return nil, err
}
return &deadlineDuration, nil
}

View File

@@ -0,0 +1,192 @@
// 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"
"errors"
"reflect"
"testing"
"time"
madmin "github.com/minio/minio/pkg/madmin"
)
// assigning mock at runtime instead of compile time
var minioServerHealthInfoMock func(ctx context.Context, healthDataTypes []madmin.HealthDataType, deadline time.Duration) <-chan madmin.HealthInfo
// mock function serverHealthInfo
func (ac adminClientMock) serverHealthInfo(ctx context.Context, healthDataTypes []madmin.HealthDataType, deadline time.Duration) <-chan madmin.HealthInfo {
return minioServerHealthInfoMock(ctx, healthDataTypes, deadline)
}
func Test_serverHealthInfo(t *testing.T) {
var testReceiver chan madmin.HealthInfo
ctx := context.Background()
client := adminClientMock{}
mockWSConn := mockConn{}
deadlineDuration, _ := time.ParseDuration("1h")
type args struct {
deadline time.Duration
wsWriteMock func(messageType int, data []byte) error
mockMessages []madmin.HealthInfo
}
tests := []struct {
test string
args args
wantError error
}{
{
test: "Return simple health info, no errors",
args: args{
deadline: deadlineDuration,
mockMessages: []madmin.HealthInfo{
madmin.HealthInfo{
Perf: madmin.PerfInfo{
NetParallel: madmin.ServerNetHealthInfo{
Addr: "someaddress",
},
},
},
madmin.HealthInfo{
Perf: madmin.PerfInfo{
NetParallel: madmin.ServerNetHealthInfo{
Addr: "otheraddress",
},
},
},
},
wsWriteMock: func(messageType int, data []byte) error {
// mock connection WriteMessage() no error
// emulate that receiver gets the message written
var t madmin.HealthInfo
_ = json.Unmarshal(data, &t)
testReceiver <- t
return nil
},
},
wantError: nil,
},
{
test: "Return simple health info2, no errors",
args: args{
deadline: deadlineDuration,
mockMessages: []madmin.HealthInfo{
madmin.HealthInfo{
Perf: madmin.PerfInfo{
NetParallel: madmin.ServerNetHealthInfo{
Addr: "address",
},
},
},
},
wsWriteMock: func(messageType int, data []byte) error {
// mock connection WriteMessage() no error
// emulate that receiver gets the message written
var t madmin.HealthInfo
_ = json.Unmarshal(data, &t)
testReceiver <- t
return nil
},
},
wantError: nil,
},
{
test: "Handle error on ws write",
args: args{
deadline: deadlineDuration,
mockMessages: []madmin.HealthInfo{
madmin.HealthInfo{
Perf: madmin.PerfInfo{
NetParallel: madmin.ServerNetHealthInfo{
Addr: "address",
},
},
},
},
wsWriteMock: func(messageType int, data []byte) error {
// mock connection WriteMessage() no error
// emulate that receiver gets the message written
var t madmin.HealthInfo
_ = json.Unmarshal(data, &t)
return errors.New("error on write")
},
},
wantError: errors.New("error on write"),
},
{
test: "Handle error on health function",
args: args{
deadline: deadlineDuration,
mockMessages: []madmin.HealthInfo{
madmin.HealthInfo{
Error: "error on healthInfo",
},
},
wsWriteMock: func(messageType int, data []byte) error {
// mock connection WriteMessage() no error
// emulate that receiver gets the message written
var t madmin.HealthInfo
_ = json.Unmarshal(data, &t)
return nil
},
},
wantError: errors.New("error on healthInfo"),
},
}
for _, tt := range tests {
t.Run(tt.test, func(t *testing.T) {
// make testReceiver channel
testReceiver = make(chan madmin.HealthInfo, len(tt.args.mockMessages))
// mock function same for all tests, changes mockMessages
minioServerHealthInfoMock = func(ctx context.Context, healthDataTypes []madmin.HealthDataType, deadline time.Duration) <-chan madmin.HealthInfo {
respChan := make(chan madmin.HealthInfo)
go func(ch chan madmin.HealthInfo) {
defer close(ch)
for _, info := range tt.args.mockMessages {
ch <- info
}
}(respChan)
return respChan
}
connWriteMessageMock = tt.args.wsWriteMock
err := startHealthInfo(ctx, mockWSConn, client, &deadlineDuration)
// close test mock channel
close(testReceiver)
// check that the TestReceiver got the same number of data from Console.
index := 0
for info := range testReceiver {
if !reflect.DeepEqual(info, tt.args.mockMessages[index]) {
t.Errorf("startHealthInfo() got: %v, want: %v", info, tt.args.mockMessages[index])
return
}
index++
}
if !reflect.DeepEqual(err, tt.wantError) {
t.Errorf("startHealthInfo() error: %v, wantError: %v", err, tt.wantError)
return
}
})
}
}

View File

@@ -22,6 +22,7 @@ import (
"net/http"
"path/filepath"
"runtime"
"time"
"github.com/minio/console/models"
mcCmd "github.com/minio/mc/cmd"
@@ -105,6 +106,8 @@ type MinioAdmin interface {
addRemoteBucket(ctx context.Context, bucket string, target *madmin.BucketTarget) (string, error)
// Account password management
changePassword(ctx context.Context, accessKey, secretKey string) error
serverHealthInfo(ctx context.Context, healthDataTypes []madmin.HealthDataType, deadline time.Duration) <-chan madmin.HealthInfo
}
// Interface implementation
@@ -286,11 +289,15 @@ func (ac adminClient) addRemoteBucket(ctx context.Context, bucket string, target
return ac.client.SetRemoteTarget(ctx, bucket, target)
}
// addRemoteBucket sets up a remote target for this bucket
func (ac adminClient) setBucketQuota(ctx context.Context, bucket string, quota *madmin.BucketQuota) error {
return ac.client.SetBucketQuota(ctx, bucket, quota)
}
// serverHealthInfo implements mc.ServerHealthInfo - Connect to a minio server and call Health Info Management API
func (ac adminClient) serverHealthInfo(ctx context.Context, healthDataTypes []madmin.HealthDataType, deadline time.Duration) <-chan madmin.HealthInfo {
return ac.client.ServerHealthInfo(ctx, healthDataTypes, deadline)
}
func newMAdminClient(sessionClaims *models.Principal) (*madmin.AdminClient, error) {
adminClient, err := newAdminFromClaims(sessionClaims)
if err != nil {

View File

@@ -80,7 +80,7 @@ func startWatch(ctx context.Context, conn WSConn, wsc MCClient, options *watchOp
// getWatchOptionsFromReq gets bucket name, events, prefix, suffix from a websocket
// watch path if defined.
// path come as : `/watch/<namespace>/<tenantName>/bucket1` and query
// path come as : `/watch/bucket1` and query
// params come on request form
func getWatchOptionsFromReq(req *http.Request) (*watchOptions, error) {
wOptions := watchOptions{}

View File

@@ -22,6 +22,7 @@ import (
"net"
"net/http"
"strings"
"time"
"github.com/go-openapi/errors"
"github.com/gorilla/websocket"
@@ -132,6 +133,19 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
return
}
go wsAdminClient.console()
case strings.HasPrefix(wsPath, `/health-info`):
deadline, err := getHealthInfoOptionsFromReq(req)
if err != nil {
log.Println("error getting health info options:", err)
closeWsConn(conn)
return
}
wsAdminClient, err := newWebSocketAdminClient(conn, session)
if err != nil {
closeWsConn(conn)
return
}
go wsAdminClient.healthInfo(deadline)
case strings.HasPrefix(wsPath, `/heal`):
hOptions, err := getHealOptionsFromReq(req)
if err != nil {
@@ -308,10 +322,26 @@ func (wsc *wsAdminClient) heal(opts *healOptions) {
sendWsCloseMessage(wsc.conn, err)
}
func (wsc *wsAdminClient) healthInfo(deadline *time.Duration) {
defer func() {
log.Println("health info stopped")
// close connection after return
wsc.conn.close()
}()
log.Println("health info started")
ctx := wsReadClientCtx(wsc.conn)
err := startHealthInfo(ctx, wsc.conn, wsc.client, deadline)
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) {
if err != nil {
log.Print("original ws error: ", err.Error())
// If connection exceeded read deadline send Close
// Message Policy Violation code since we don't want
// to let the receiver figure out the read deadline.
@@ -322,7 +352,7 @@ func sendWsCloseMessage(conn WSConn, err error) {
return
}
// else, internal server error
conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, err.Error()))
conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, errorGeneric.Error()))
return
}
// normal closure