github.com/minio/console@v1.4.1/web-app/src/screens/Console/Support/Profile.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 { Button, PageLayout, FormLayout, Box, Checkbox, InputLabel } from "mds"; 19 import { wsProtocol } from "../../../utils/wsUtils"; 20 import { useNavigate } from "react-router-dom"; 21 import { registeredCluster } from "../../../config"; 22 import { useAppDispatch } from "../../../store"; 23 import { setHelpName } from "../../../systemSlice"; 24 import RegisterCluster from "./RegisterCluster"; 25 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 26 import HelpMenu from "../HelpMenu"; 27 28 var socket: any = null; 29 30 const Profile = () => { 31 const navigate = useNavigate(); 32 33 const [profilingStarted, setProfilingStarted] = useState<boolean>(false); 34 const [types, setTypes] = useState<string[]>([ 35 "cpu", 36 "mem", 37 "block", 38 "mutex", 39 "goroutines", 40 ]); 41 const clusterRegistered = registeredCluster(); 42 const typesList = [ 43 { label: "cpu", value: "cpu" }, 44 { label: "mem", value: "mem" }, 45 { label: "block", value: "block" }, 46 { label: "mutex", value: "mutex" }, 47 { label: "goroutines", value: "goroutines" }, 48 ]; 49 50 const onCheckboxClick = (e: React.ChangeEvent<HTMLInputElement>) => { 51 let newArr: string[] = []; 52 if (types.indexOf(e.target.value) > -1) { 53 newArr = types.filter((type) => type !== e.target.value); 54 } else { 55 newArr = [...types, e.target.value]; 56 } 57 setTypes(newArr); 58 }; 59 60 const startProfiling = () => { 61 const typeString = types.join(","); 62 63 const url = new URL(window.location.toString()); 64 const isDev = process.env.NODE_ENV === "development"; 65 const port = isDev ? "9090" : url.port; 66 67 // check if we are using base path, if not this always is `/` 68 const baseLocation = new URL(document.baseURI); 69 const baseUrl = baseLocation.pathname; 70 71 const wsProt = wsProtocol(url.protocol); 72 socket = new WebSocket( 73 `${wsProt}://${url.hostname}:${port}${baseUrl}ws/profile?types=${typeString}`, 74 ); 75 76 if (socket !== null) { 77 socket.onopen = () => { 78 setProfilingStarted(true); 79 socket.send("ok"); 80 }; 81 socket.onmessage = (message: MessageEvent) => { 82 // process received message 83 let response = new Blob([message.data], { type: "application/zip" }); 84 let filename = "profile.zip"; 85 setProfilingStarted(false); 86 var link = document.createElement("a"); 87 link.href = window.URL.createObjectURL(response); 88 link.download = filename; 89 document.body.appendChild(link); 90 link.click(); 91 document.body.removeChild(link); 92 }; 93 socket.onclose = () => { 94 console.log("connection closed by server"); 95 setProfilingStarted(false); 96 }; 97 return () => { 98 socket.close(1000); 99 console.log("closing websockets"); 100 setProfilingStarted(false); 101 }; 102 } 103 }; 104 105 const stopProfiling = () => { 106 socket.close(1000); 107 setProfilingStarted(false); 108 }; 109 110 const dispatch = useAppDispatch(); 111 useEffect(() => { 112 dispatch(setHelpName("profile")); 113 // eslint-disable-next-line react-hooks/exhaustive-deps 114 }, []); 115 116 return ( 117 <Fragment> 118 <PageHeaderWrapper label="Profile" actions={<HelpMenu />} /> 119 <PageLayout> 120 {!clusterRegistered && <RegisterCluster compactMode />} 121 <FormLayout> 122 <Box 123 sx={{ 124 display: "flex", 125 gap: 10, 126 "& div": { width: "initial" }, 127 "& .inputItem:not(:last-of-type)": { marginBottom: 0 }, 128 }} 129 > 130 <InputLabel noMinWidth>Types to profile:</InputLabel> 131 {typesList.map((t) => ( 132 <Checkbox 133 checked={types.indexOf(t.value) > -1} 134 disabled={profilingStarted || !clusterRegistered} 135 key={`checkbox-${t.label}`} 136 id={`checkbox-${t.label}`} 137 label={t.label} 138 name={`checkbox-${t.label}`} 139 onChange={onCheckboxClick} 140 value={t.value} 141 /> 142 ))} 143 </Box> 144 <Box 145 sx={{ 146 display: "flex", 147 justifyContent: "flex-end", 148 marginTop: 24, 149 gap: 10, 150 }} 151 > 152 <Button 153 id={"start-profiling"} 154 type="submit" 155 variant={clusterRegistered ? "callAction" : "regular"} 156 disabled={ 157 profilingStarted || types.length < 1 || !clusterRegistered 158 } 159 onClick={() => { 160 if (!clusterRegistered) { 161 navigate("/support/register"); 162 return; 163 } 164 startProfiling(); 165 }} 166 label={"Start Profiling"} 167 /> 168 <Button 169 id={"stop-profiling"} 170 type="submit" 171 variant="callAction" 172 color="primary" 173 disabled={!profilingStarted || !clusterRegistered} 174 onClick={() => { 175 stopProfiling(); 176 }} 177 label={"Stop Profiling"} 178 /> 179 </Box> 180 </FormLayout> 181 </PageLayout> 182 </Fragment> 183 ); 184 }; 185 186 export default Profile;