github.com/minio/console@v1.4.1/web-app/src/screens/Console/KMS/Status.tsx (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2022 MinIO, Inc. 3 // 4 // This program is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Affero General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // This program is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17 import React, { Fragment, useEffect, useState } from "react"; 18 import { 19 Box, 20 breakPoints, 21 DisabledIcon, 22 EnabledIcon, 23 Grid, 24 PageLayout, 25 SectionTitle, 26 Tabs, 27 ValuePair, 28 } from "mds"; 29 import { 30 Bar, 31 BarChart, 32 CartesianGrid, 33 Legend, 34 Line, 35 LineChart, 36 Tooltip, 37 XAxis, 38 YAxis, 39 } from "recharts"; 40 import { hasPermission } from "../../../common/SecureComponent"; 41 import { 42 CONSOLE_UI_RESOURCE, 43 IAM_SCOPES, 44 } from "../../../common/SecureComponent/permissions"; 45 import { setErrorSnackMessage, setHelpName } from "../../../systemSlice"; 46 import { useAppDispatch } from "../../../store"; 47 import LabelWithIcon from "../Buckets/BucketDetails/SummaryItems/LabelWithIcon"; 48 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 49 import HelpMenu from "../HelpMenu"; 50 import { api } from "api"; 51 import { KmsStatusResponse } from "api/consoleApi"; 52 import { errorToHandler } from "api/errors"; 53 54 const Status = () => { 55 const dispatch = useAppDispatch(); 56 const [curTab, setCurTab] = useState<string>("simple-tab-0"); 57 58 const [isKMSSecretKey, setIsKMSSecretKey] = useState<boolean>(true); 59 const [status, setStatus] = useState<KmsStatusResponse | null>(null); 60 const [loadingStatus, setLoadingStatus] = useState<boolean>(true); 61 const [metrics, setMetrics] = useState<any | null>(null); 62 const [loadingMetrics, setLoadingMetrics] = useState<boolean>(true); 63 const [apis, setAPIs] = useState<any | null>(null); 64 const [loadingAPIs, setLoadingAPIs] = useState<boolean>(true); 65 const [version, setVersion] = useState<any | null>(null); 66 const [loadingVersion, setLoadingVersion] = useState<boolean>(true); 67 68 const displayStatus = hasPermission(CONSOLE_UI_RESOURCE, [ 69 IAM_SCOPES.KMS_STATUS, 70 ]); 71 const displayMetrics = 72 hasPermission(CONSOLE_UI_RESOURCE, [IAM_SCOPES.KMS_METRICS]) && 73 !isKMSSecretKey; 74 const displayAPIs = 75 hasPermission(CONSOLE_UI_RESOURCE, [IAM_SCOPES.KMS_APIS]) && 76 !isKMSSecretKey; 77 const displayVersion = 78 hasPermission(CONSOLE_UI_RESOURCE, [IAM_SCOPES.KMS_Version]) && 79 !isKMSSecretKey; 80 81 useEffect(() => { 82 const loadStatus = () => { 83 api.kms 84 .kmsStatus() 85 .then((result) => { 86 if (result.data) { 87 setStatus(result.data); 88 setIsKMSSecretKey(result.data.name === "SecretKey"); 89 } 90 }) 91 .catch((err) => { 92 dispatch(setErrorSnackMessage(errorToHandler(err.error))); 93 }) 94 .finally(() => setLoadingStatus(false)); 95 }; 96 97 const loadMetrics = () => { 98 api.kms 99 .kmsMetrics() 100 .then((result) => { 101 if (result.data) { 102 setMetrics(result.data); 103 } 104 }) 105 .catch((err) => { 106 dispatch(setErrorSnackMessage(errorToHandler(err.error))); 107 }) 108 .finally(() => setLoadingMetrics(false)); 109 }; 110 111 const loadAPIs = () => { 112 api.kms 113 .kmsapIs() 114 .then((result: any) => { 115 if (result.data) { 116 setAPIs(result.data); 117 } 118 }) 119 .catch((err) => { 120 dispatch(setErrorSnackMessage(errorToHandler(err.error))); 121 }) 122 .finally(() => setLoadingAPIs(false)); 123 }; 124 125 const loadVersion = () => { 126 api.kms 127 .kmsVersion() 128 .then((result: any) => { 129 if (result.data) { 130 setVersion(result.data); 131 } 132 }) 133 .catch((err) => { 134 dispatch(setErrorSnackMessage(errorToHandler(err.error))); 135 }) 136 .finally(() => setLoadingVersion(false)); 137 }; 138 139 if (displayStatus && loadingStatus) { 140 loadStatus(); 141 } 142 if (displayMetrics && loadingMetrics) { 143 loadMetrics(); 144 } 145 if (displayAPIs && loadingAPIs) { 146 loadAPIs(); 147 } 148 if (displayVersion && loadingVersion) { 149 loadVersion(); 150 } 151 }, [ 152 dispatch, 153 displayStatus, 154 loadingStatus, 155 displayMetrics, 156 loadingMetrics, 157 displayAPIs, 158 loadingAPIs, 159 displayVersion, 160 loadingVersion, 161 ]); 162 163 const statusPanel = ( 164 <Fragment> 165 <SectionTitle>Status</SectionTitle> 166 <br /> 167 {status && ( 168 <Grid container> 169 <Grid item xs={12}> 170 <Box 171 sx={{ 172 display: "grid", 173 gap: 2, 174 gridTemplateColumns: "2fr 1fr", 175 gridAutoFlow: "row", 176 [`@media (max-width: ${breakPoints.sm}px)`]: { 177 gridTemplateColumns: "1fr", 178 gridAutoFlow: "dense", 179 }, 180 }} 181 > 182 <Box 183 sx={{ 184 display: "grid", 185 gap: 2, 186 gridTemplateColumns: "2fr 1fr", 187 gridAutoFlow: "row", 188 [`@media (max-width: ${breakPoints.sm}px)`]: { 189 gridTemplateColumns: "1fr", 190 gridAutoFlow: "dense", 191 }, 192 }} 193 > 194 <ValuePair label={"Name:"} value={status.name} /> 195 {version && ( 196 <ValuePair label={"Version:"} value={version.version} /> 197 )} 198 <ValuePair 199 label={"Default Key ID:"} 200 value={status.defaultKeyID} 201 /> 202 <ValuePair 203 label={"Key Management Service Endpoints:"} 204 value={ 205 <Fragment> 206 {status.endpoints?.map((e: any, i: number) => ( 207 <LabelWithIcon 208 key={i} 209 icon={ 210 e.status === "online" ? ( 211 <EnabledIcon /> 212 ) : ( 213 <DisabledIcon /> 214 ) 215 } 216 label={e.url} 217 /> 218 ))} 219 </Fragment> 220 } 221 /> 222 </Box> 223 </Box> 224 </Grid> 225 </Grid> 226 )} 227 </Fragment> 228 ); 229 230 const apisPanel = ( 231 <Fragment> 232 <SectionTitle>Supported API endpoints</SectionTitle> 233 <br /> 234 {apis && ( 235 <Grid container> 236 <Grid item xs={12}> 237 <ValuePair 238 label={""} 239 value={ 240 <Box 241 sx={{ 242 display: "grid", 243 gap: 2, 244 gridTemplateColumns: "2fr 1fr", 245 gridAutoFlow: "row", 246 [`@media (max-width: ${breakPoints.sm}px)`]: { 247 gridTemplateColumns: "1fr", 248 gridAutoFlow: "dense", 249 }, 250 }} 251 > 252 {apis.results.map((e: any, i: number) => ( 253 <LabelWithIcon 254 key={i} 255 icon={<EnabledIcon />} 256 label={`${e.path} - ${e.method}`} 257 /> 258 ))} 259 </Box> 260 } 261 /> 262 </Grid> 263 </Grid> 264 )} 265 </Fragment> 266 ); 267 268 const getAPIRequestsData = () => { 269 return [ 270 { label: "Success", success: metrics.requestOK }, 271 { label: "Failures", failures: metrics.requestFail }, 272 { label: "Errors", errors: metrics.requestErr }, 273 { label: "Active", active: metrics.requestActive }, 274 ]; 275 }; 276 277 const getEventsData = () => { 278 return [ 279 { label: "Audit", audit: metrics.auditEvents }, 280 { label: "Errors", errors: metrics.errorEvents }, 281 ]; 282 }; 283 284 const getHistogramData = () => { 285 return metrics.latencyHistogram.map((h: any) => { 286 return { 287 ...h, 288 duration: `${h.duration / 1000000}ms`, 289 }; 290 }); 291 }; 292 293 const metricsPanel = ( 294 <Fragment> 295 {metrics && ( 296 <Fragment> 297 <h3>API Requests</h3> 298 <BarChart width={730} height={250} data={getAPIRequestsData()}> 299 <CartesianGrid strokeDasharray="3 3" /> 300 <XAxis dataKey="label" /> 301 <YAxis /> 302 <Tooltip /> 303 <Legend /> 304 <Bar dataKey="success" fill="green" /> 305 <Bar dataKey="failures" fill="red" /> 306 <Bar dataKey="errors" fill="black" /> 307 <Bar dataKey="active" fill="#8884d8" /> 308 </BarChart> 309 310 <h3>Events</h3> 311 <BarChart width={730} height={250} data={getEventsData()}> 312 <CartesianGrid strokeDasharray="3 3" /> 313 <XAxis dataKey="label" /> 314 <YAxis /> 315 <Tooltip /> 316 <Legend /> 317 <Bar dataKey="audit" fill="green" /> 318 <Bar dataKey="errors" fill="black" /> 319 </BarChart> 320 <h3>Latency Histogram</h3> 321 {metrics.latencyHistogram && ( 322 <LineChart 323 width={730} 324 height={250} 325 data={getHistogramData()} 326 margin={{ top: 5, right: 30, left: 20, bottom: 5 }} 327 > 328 <CartesianGrid strokeDasharray="3 3" /> 329 <XAxis dataKey="duration" /> 330 <YAxis /> 331 <Tooltip /> 332 <Legend /> 333 <Line 334 type="monotone" 335 dataKey="total" 336 stroke="#8884d8" 337 name={"Requests that took T ms or less"} 338 /> 339 </LineChart> 340 )} 341 </Fragment> 342 )} 343 </Fragment> 344 ); 345 346 useEffect(() => { 347 dispatch(setHelpName("kms_status")); 348 // eslint-disable-next-line react-hooks/exhaustive-deps 349 }, []); 350 351 return ( 352 <Fragment> 353 <PageHeaderWrapper 354 label="Key Management Service" 355 actions={<HelpMenu />} 356 /> 357 358 <PageLayout> 359 <Tabs 360 currentTabOrPath={curTab} 361 onTabClick={(newValue) => setCurTab(newValue)} 362 options={[ 363 { 364 tabConfig: { label: "Status", id: "simple-tab-0" }, 365 content: ( 366 <Box 367 withBorders 368 sx={{ 369 display: "flex", 370 flexFlow: "column", 371 padding: "43px", 372 }} 373 > 374 {statusPanel} 375 </Box> 376 ), 377 }, 378 { 379 tabConfig: { 380 label: "APIs", 381 id: "simple-tab-1", 382 disabled: !displayAPIs, 383 }, 384 content: ( 385 <Box 386 withBorders 387 sx={{ 388 display: "flex", 389 flexFlow: "column", 390 padding: "43px", 391 }} 392 > 393 {apisPanel} 394 </Box> 395 ), 396 }, 397 { 398 tabConfig: { 399 label: "Metrics", 400 id: "simple-tab-2", 401 disabled: !displayMetrics, 402 }, 403 content: ( 404 <Box 405 withBorders 406 sx={{ 407 display: "flex", 408 flexFlow: "column", 409 padding: "43px", 410 }} 411 > 412 {metricsPanel} 413 </Box> 414 ), 415 }, 416 ]} 417 /> 418 </PageLayout> 419 </Fragment> 420 ); 421 }; 422 423 export default Status;