github.com/minio/console@v1.4.1/web-app/src/screens/Console/Watch/Watch.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 17 import React, { useEffect, useState, Fragment } from "react"; 18 import { useSelector } from "react-redux"; 19 import { 20 Box, 21 Button, 22 DataTable, 23 Grid, 24 InputBox, 25 InputLabel, 26 PageLayout, 27 Select, 28 } from "mds"; 29 import { AppState, useAppDispatch } from "../../../store"; 30 import { Bucket, BucketList, EventInfo } from "./types"; 31 import { niceBytes, timeFromDate } from "../../../common/utils"; 32 import { wsProtocol } from "../../../utils/wsUtils"; 33 import { ErrorResponseHandler } from "../../../common/types"; 34 import { watchMessageReceived, watchResetMessages } from "./watchSlice"; 35 import { setHelpName } from "../../../systemSlice"; 36 import api from "../../../common/api"; 37 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 38 import HelpMenu from "../HelpMenu"; 39 40 const Watch = () => { 41 const dispatch = useAppDispatch(); 42 const messages = useSelector((state: AppState) => state.watch.messages); 43 44 const [start, setStart] = useState(false); 45 const [bucketName, setBucketName] = useState("Select Bucket"); 46 const [prefix, setPrefix] = useState(""); 47 const [suffix, setSuffix] = useState(""); 48 const [bucketList, setBucketList] = useState<Bucket[]>([]); 49 50 const fetchBucketList = () => { 51 api 52 .invoke("GET", `/api/v1/buckets`) 53 .then((res: BucketList) => { 54 let buckets: Bucket[] = []; 55 if (res.buckets !== null) { 56 buckets = res.buckets; 57 } 58 setBucketList(buckets); 59 }) 60 .catch((err: ErrorResponseHandler) => { 61 console.error(err); 62 }); 63 }; 64 useEffect(() => { 65 fetchBucketList(); 66 }, []); 67 68 useEffect(() => { 69 dispatch(watchResetMessages()); 70 // begin watch if bucketName in bucketList and start pressed 71 if (start && bucketList.some((bucket) => bucket.name === bucketName)) { 72 const url = new URL(window.location.toString()); 73 const isDev = process.env.NODE_ENV === "development"; 74 const port = isDev ? "9090" : url.port; 75 76 // check if we are using base path, if not this always is `/` 77 const baseLocation = new URL(document.baseURI); 78 const baseUrl = baseLocation.pathname; 79 80 const wsProt = wsProtocol(url.protocol); 81 const socket = new WebSocket( 82 `${wsProt}://${url.hostname}:${port}${baseUrl}ws/watch/${bucketName}?prefix=${prefix}&suffix=${suffix}`, 83 ); 84 85 let interval: any | null = null; 86 if (socket !== null) { 87 socket.onopen = () => { 88 console.log("WebSocket Client Connected"); 89 socket.send("ok"); 90 interval = setInterval(() => { 91 socket.send("ok"); 92 }, 10 * 1000); 93 }; 94 socket.onmessage = (message: MessageEvent) => { 95 let m: EventInfo = JSON.parse(message.data.toString()); 96 m.Time = new Date(m.Time.toString()); 97 m.key = Math.random(); 98 dispatch(watchMessageReceived(m)); 99 }; 100 socket.onclose = () => { 101 clearInterval(interval); 102 console.log("connection closed by server"); 103 // reset start status 104 setStart(false); 105 }; 106 return () => { 107 // close websocket on useEffect cleanup 108 socket.close(1000); 109 clearInterval(interval); 110 console.log("closing websockets"); 111 }; 112 } 113 } else { 114 // reset start status 115 setStart(false); 116 } 117 }, [dispatch, start, bucketList, bucketName, prefix, suffix]); 118 119 const bucketNames = bucketList.map((bucketName) => ({ 120 label: bucketName.name, 121 value: bucketName.name, 122 })); 123 124 useEffect(() => { 125 dispatch(setHelpName("watch")); 126 // eslint-disable-next-line react-hooks/exhaustive-deps 127 }, []); 128 129 const optionsArray = bucketNames.map((option) => ({ 130 label: option.label, 131 value: option.value, 132 })); 133 134 return ( 135 <Fragment> 136 <PageHeaderWrapper label="Watch" actions={<HelpMenu />} /> 137 <PageLayout> 138 <Grid container> 139 <Grid 140 item 141 xs={12} 142 sx={{ 143 display: "flex", 144 gap: 10, 145 marginBottom: 15, 146 alignItems: "center", 147 }} 148 > 149 <Box sx={{ flexGrow: 1 }}> 150 <InputLabel>Bucket</InputLabel> 151 <Select 152 id="bucket-name" 153 name="bucket-name" 154 value={bucketName} 155 onChange={(value) => { 156 setBucketName(value as string); 157 }} 158 disabled={start} 159 options={optionsArray} 160 placeholder={"Select Bucket"} 161 /> 162 </Box> 163 <Box sx={{ flexGrow: 1 }}> 164 <InputLabel>Prefix</InputLabel> 165 <InputBox 166 id="prefix-resource" 167 disabled={start} 168 onChange={(e) => { 169 setPrefix(e.target.value); 170 }} 171 /> 172 </Box> 173 <Box sx={{ flexGrow: 1 }}> 174 <InputLabel>Suffix</InputLabel> 175 <InputBox 176 id="suffix-resource" 177 disabled={start} 178 onChange={(e) => { 179 setSuffix(e.target.value); 180 }} 181 /> 182 </Box> 183 <Box sx={{ alignSelf: "flex-end", paddingBottom: 4 }}> 184 {start ? ( 185 <Button 186 id={"stop-watch"} 187 type="submit" 188 variant="callAction" 189 onClick={() => setStart(false)} 190 label={"Stop"} 191 /> 192 ) : ( 193 <Button 194 id={"start-watch"} 195 type="submit" 196 variant="callAction" 197 onClick={() => setStart(true)} 198 label={"Start"} 199 /> 200 )} 201 </Box> 202 </Grid> 203 204 <Grid item xs={12}> 205 <DataTable 206 columns={[ 207 { 208 label: "Time", 209 elementKey: "Time", 210 renderFunction: timeFromDate, 211 }, 212 { 213 label: "Size", 214 elementKey: "Size", 215 renderFunction: niceBytes, 216 }, 217 { label: "Type", elementKey: "Type" }, 218 { label: "Path", elementKey: "Path" }, 219 ]} 220 records={messages} 221 entityName={"Watch"} 222 customEmptyMessage={"No Changes at this time"} 223 idField={"watch_table"} 224 isLoading={false} 225 customPaperHeight={"calc(100vh - 270px)"} 226 /> 227 </Grid> 228 </Grid> 229 </PageLayout> 230 </Fragment> 231 ); 232 }; 233 234 export default Watch;