github.com/minio/console@v1.4.1/web-app/src/screens/Console/HealthInfo/HealthInfo.tsx (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2021 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 import React, { Fragment, useEffect, useState } from "react"; 17 import { useSelector } from "react-redux"; 18 import { useNavigate } from "react-router-dom"; 19 import { Box, Button, Grid, HelpBox, InfoIcon, Loader, PageLayout } from "mds"; 20 import { 21 DiagStatError, 22 DiagStatInProgress, 23 DiagStatSuccess, 24 HealthInfoMessage, 25 ReportMessage, 26 } from "./types"; 27 import { AppState, useAppDispatch } from "../../../store"; 28 import { 29 WSCloseAbnormalClosure, 30 WSCloseInternalServerErr, 31 WSClosePolicyViolation, 32 wsProtocol, 33 } from "../../../utils/wsUtils"; 34 import { setHelpName, setServerDiagStat } from "../../../systemSlice"; 35 import { 36 healthInfoMessageReceived, 37 healthInfoResetMessage, 38 } from "./healthInfoSlice"; 39 import { registeredCluster } from "../../../config"; 40 import TestWrapper from "../Common/TestWrapper/TestWrapper"; 41 import RegisterCluster from "../Support/RegisterCluster"; 42 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 43 import HelpMenu from "../HelpMenu"; 44 45 const HealthInfo = () => { 46 const dispatch = useAppDispatch(); 47 const navigate = useNavigate(); 48 49 const message = useSelector((state: AppState) => state.healthInfo.message); 50 51 const serverDiagnosticStatus = useSelector( 52 (state: AppState) => state.system.serverDiagnosticStatus, 53 ); 54 const [startDiagnostic, setStartDiagnostic] = useState(false); 55 56 const [downloadDisabled, setDownloadDisabled] = useState(true); 57 const [localMessage, setMessage] = useState<string>(""); 58 const [buttonStartText, setButtonStartText] = useState<string>( 59 "Start Health Report", 60 ); 61 const [title, setTitle] = useState<string>("Health Report"); 62 const [diagFileContent, setDiagFileContent] = useState<string>(""); 63 const [subnetResponse, setSubnetResponse] = useState<string>(""); 64 const clusterRegistered = registeredCluster(); 65 66 const download = () => { 67 let element = document.createElement("a"); 68 element.setAttribute( 69 "href", 70 `data:application/gzip;base64,${diagFileContent}`, 71 ); 72 element.setAttribute("download", "diagnostic.json.gz"); 73 74 element.style.display = "none"; 75 document.body.appendChild(element); 76 77 element.click(); 78 79 document.body.removeChild(element); 80 }; 81 82 useEffect(() => { 83 if (serverDiagnosticStatus === DiagStatInProgress) { 84 setTitle("Health Report in progress..."); 85 setMessage( 86 "Health Report started. Please do not refresh page during diagnosis.", 87 ); 88 return; 89 } 90 91 if (serverDiagnosticStatus === DiagStatSuccess) { 92 setTitle("Health Report complete"); 93 setMessage("Health Report file is ready to be downloaded."); 94 setButtonStartText("Start Health Report"); 95 return; 96 } 97 98 if (serverDiagnosticStatus === DiagStatError) { 99 setTitle("Error"); 100 setMessage("An error occurred while getting the Health Report file."); 101 setButtonStartText("Retry Health Report"); 102 return; 103 } 104 }, [serverDiagnosticStatus, startDiagnostic]); 105 106 useEffect(() => { 107 if ( 108 serverDiagnosticStatus === DiagStatSuccess && 109 message !== ({} as HealthInfoMessage) 110 ) { 111 // Allow download of diagnostics file only when 112 // it succeded fetching all the results and info is not empty. 113 setDownloadDisabled(false); 114 } 115 if (serverDiagnosticStatus === DiagStatInProgress) { 116 // Disable Start Health Report and Disable Download buttons 117 // if a Diagnosis is in progress. 118 setDownloadDisabled(true); 119 } 120 setStartDiagnostic(false); 121 }, [serverDiagnosticStatus, message]); 122 123 useEffect(() => { 124 if (startDiagnostic) { 125 dispatch(healthInfoResetMessage()); 126 setDiagFileContent(""); 127 const url = new URL(window.location.toString()); 128 const isDev = process.env.NODE_ENV === "development"; 129 const port = isDev ? "9090" : url.port; 130 131 const wsProt = wsProtocol(url.protocol); 132 133 // check if we are using base path, if not this always is `/` 134 const baseLocation = new URL(document.baseURI); 135 const baseUrl = baseLocation.pathname; 136 137 const socket = new WebSocket( 138 `${wsProt}://${url.hostname}:${port}${baseUrl}ws/health-info?deadline=1h`, 139 ); 140 let interval: any | null = null; 141 if (socket !== null) { 142 socket.onopen = () => { 143 console.log("WebSocket Client Connected"); 144 socket.send("ok"); 145 interval = setInterval(() => { 146 socket.send("ok"); 147 }, 10 * 1000); 148 setMessage( 149 "Health Report started. Please do not refresh page during diagnosis.", 150 ); 151 dispatch(setServerDiagStat(DiagStatInProgress)); 152 }; 153 socket.onmessage = (message: MessageEvent) => { 154 let m: ReportMessage = JSON.parse(message.data.toString()); 155 if (m.serverHealthInfo) { 156 dispatch(healthInfoMessageReceived(m.serverHealthInfo)); 157 } 158 if (m.encoded !== "") { 159 setDiagFileContent(m.encoded); 160 } 161 if (m.subnetResponse) { 162 setSubnetResponse(m.subnetResponse); 163 } 164 }; 165 socket.onerror = (error) => { 166 console.error("error closing websocket:", error); 167 socket.close(1000); 168 clearInterval(interval); 169 dispatch(setServerDiagStat(DiagStatError)); 170 }; 171 socket.onclose = (event: CloseEvent) => { 172 clearInterval(interval); 173 if ( 174 event.code === WSCloseInternalServerErr || 175 event.code === WSClosePolicyViolation || 176 event.code === WSCloseAbnormalClosure 177 ) { 178 // handle close with error 179 console.log("connection closed by server with code:", event.code); 180 setMessage( 181 "An error occurred while getting the Health Report file.", 182 ); 183 dispatch(setServerDiagStat(DiagStatError)); 184 } else { 185 console.log("connection closed by server"); 186 187 setMessage("Health Report file is ready to be downloaded."); 188 dispatch(setServerDiagStat(DiagStatSuccess)); 189 } 190 }; 191 } 192 } else { 193 // reset start status 194 setStartDiagnostic(false); 195 } 196 }, [startDiagnostic, dispatch]); 197 198 const startDiagnosticAction = () => { 199 if (!clusterRegistered) { 200 navigate("/support/register"); 201 return; 202 } 203 setStartDiagnostic(true); 204 }; 205 206 useEffect(() => { 207 dispatch(setHelpName("health_info")); 208 }, [dispatch]); 209 210 return ( 211 <Fragment> 212 <PageHeaderWrapper label="Health" actions={<HelpMenu />} /> 213 214 <PageLayout> 215 {!clusterRegistered && <RegisterCluster compactMode />} 216 <Box withBorders> 217 <TestWrapper title={title}> 218 <Grid 219 container 220 sx={{ 221 justifyContent: "flex-start", 222 gap: 20, 223 }} 224 > 225 <Grid 226 key="start-download" 227 item 228 xs={12} 229 sx={{ 230 textAlign: "center", 231 marginBottom: 25, 232 }} 233 > 234 <h2>{localMessage}</h2> 235 <Box 236 sx={{ 237 textAlign: "center", 238 marginBottom: 25, 239 }} 240 > 241 {" "} 242 {subnetResponse !== "" && 243 !subnetResponse.toLowerCase().includes("error") && ( 244 <Grid item xs={12}> 245 <strong> 246 Health report uploaded to SUBNET successfully! 247 </strong> 248 {" "} 249 <strong> 250 See the results on your{" "} 251 <a href={subnetResponse}>SUBNET Dashboard</a>{" "} 252 </strong> 253 </Grid> 254 )} 255 {(subnetResponse === "" || 256 subnetResponse.toLowerCase().includes("error")) && 257 serverDiagnosticStatus === DiagStatSuccess && ( 258 <Grid item xs={12}> 259 <strong> 260 Something went wrong uploading your Health report to 261 SUBNET. 262 </strong> 263 {" "} 264 <strong> 265 Log into your{" "} 266 <a href="https://subnet.min.io">SUBNET Account</a> to 267 manually upload your Health report. 268 </strong> 269 </Grid> 270 )} 271 </Box> 272 {serverDiagnosticStatus === DiagStatInProgress ? ( 273 <Box 274 sx={{ 275 paddingTop: 8, 276 paddingLeft: 40, 277 }} 278 > 279 <Loader style={{ width: 25, height: 25 }} /> 280 </Box> 281 ) : ( 282 <Fragment> 283 <Box 284 sx={{ 285 display: "flex", 286 gap: 10, 287 alignItems: "center", 288 justifyContent: "center", 289 }} 290 > 291 <Box> 292 {serverDiagnosticStatus !== DiagStatError && 293 !downloadDisabled && ( 294 <Button 295 id={"download"} 296 type="submit" 297 variant="callAction" 298 onClick={() => download()} 299 disabled={downloadDisabled} 300 label={"Download"} 301 /> 302 )} 303 </Box> 304 <Box> 305 <Button 306 id="start-new-diagnostic" 307 type="submit" 308 variant={ 309 !clusterRegistered ? "regular" : "callAction" 310 } 311 disabled={startDiagnostic || !clusterRegistered} 312 onClick={startDiagnosticAction} 313 label={buttonStartText} 314 /> 315 </Box> 316 </Box> 317 </Fragment> 318 )} 319 </Grid> 320 </Grid> 321 </TestWrapper> 322 </Box> 323 {!startDiagnostic && clusterRegistered && ( 324 <Fragment> 325 <br /> 326 <HelpBox 327 title={ 328 "Cluster Health Report will be uploaded to SUBNET, and is viewable from your SUBNET Diagnostics dashboard." 329 } 330 iconComponent={<InfoIcon />} 331 help={ 332 "If the Health report cannot be generated at this time, please wait a moment and try again." 333 } 334 /> 335 </Fragment> 336 )} 337 </PageLayout> 338 </Fragment> 339 ); 340 }; 341 342 export default HealthInfo;