github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/ListBuckets/BulkLifecycleModal.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 CheckCircleIcon, 21 FormLayout, 22 Grid, 23 InputBox, 24 RadioGroup, 25 ReadBox, 26 Select, 27 Switch, 28 Tooltip, 29 WarnIcon, 30 Wizard, 31 } from "mds"; 32 import get from "lodash/get"; 33 import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper"; 34 import QueryMultiSelector from "../../Common/FormComponents/QueryMultiSelector/QueryMultiSelector"; 35 import { ITiersDropDown } from "../types"; 36 import { setModalErrorSnackMessage } from "../../../../systemSlice"; 37 import { useAppDispatch } from "../../../../store"; 38 import { api } from "api"; 39 import { MultiLifecycleResult, Tier } from "api/consoleApi"; 40 import { errorToHandler } from "api/errors"; 41 42 interface IBulkReplicationModal { 43 open: boolean; 44 closeModalAndRefresh: (clearSelection: boolean) => any; 45 buckets: string[]; 46 } 47 48 const AddBulkReplicationModal = ({ 49 open, 50 closeModalAndRefresh, 51 buckets, 52 }: IBulkReplicationModal) => { 53 const dispatch = useAppDispatch(); 54 const [addLoading, setAddLoading] = useState<boolean>(false); 55 const [loadingTiers, setLoadingTiers] = useState<boolean>(true); 56 const [tiersList, setTiersList] = useState<ITiersDropDown[]>([]); 57 const [prefix, setPrefix] = useState(""); 58 const [tags, setTags] = useState<string>(""); 59 const [storageClass, setStorageClass] = useState(""); 60 const [NCTransitionSC, setNCTransitionSC] = useState(""); 61 const [expiredObjectDM, setExpiredObjectDM] = useState<boolean>(false); 62 const [expiredAllVersionsDM, setExpiredAllVersionsDM] = 63 useState<boolean>(false); 64 const [NCExpirationDays, setNCExpirationDays] = useState<string>("0"); 65 const [NCTransitionDays, setNCTransitionDays] = useState<string>("0"); 66 const [ilmType, setIlmType] = useState<"expiry" | "transition">("expiry"); 67 const [expiryDays, setExpiryDays] = useState<string>("0"); 68 const [transitionDays, setTransitionDays] = useState<string>("0"); 69 const [isFormValid, setIsFormValid] = useState<boolean>(false); 70 const [results, setResults] = useState<MultiLifecycleResult | null>(null); 71 72 useEffect(() => { 73 if (loadingTiers) { 74 api.admin 75 .tiersList() 76 .then((res) => { 77 const tiersList: Tier[] | null = get(res.data, "items", []); 78 79 if (tiersList !== null && tiersList.length >= 1) { 80 const objList = tiersList.map((tier: Tier) => { 81 const tierType = tier.type; 82 const value = get(tier, `${tierType}.name`, ""); 83 84 return { label: value, value: value }; 85 }); 86 87 setTiersList(objList); 88 if (objList.length > 0) { 89 setStorageClass(objList[0].value); 90 } 91 } 92 setLoadingTiers(false); 93 }) 94 .catch((err) => { 95 setLoadingTiers(false); 96 dispatch(setModalErrorSnackMessage(errorToHandler(err.error))); 97 }); 98 } 99 }, [loadingTiers, dispatch]); 100 101 useEffect(() => { 102 let valid = true; 103 104 if (ilmType !== "expiry") { 105 if (storageClass === "") { 106 valid = false; 107 } 108 } 109 setIsFormValid(valid); 110 }, [ilmType, expiryDays, transitionDays, storageClass]); 111 112 const LogoToShow = ({ errString }: { errString: string }) => { 113 switch (errString) { 114 case "": 115 return ( 116 <Box 117 sx={{ 118 paddingTop: 5, 119 color: "#42C91A", 120 }} 121 > 122 <CheckCircleIcon /> 123 </Box> 124 ); 125 case "n/a": 126 return null; 127 default: 128 if (errString) { 129 return ( 130 <Box 131 sx={{ 132 paddingTop: 5, 133 color: "#C72C48", 134 }} 135 > 136 <Tooltip tooltip={errString} placement="top"> 137 <WarnIcon /> 138 </Tooltip> 139 </Box> 140 ); 141 } 142 } 143 return null; 144 }; 145 146 const createLifecycleRules = (to: any) => { 147 let rules = {}; 148 149 if (ilmType === "expiry") { 150 let expiry = { 151 expiry_days: parseInt(expiryDays), 152 }; 153 154 rules = { 155 ...expiry, 156 noncurrentversion_expiration_days: parseInt(NCExpirationDays), 157 }; 158 } else { 159 let transition = { 160 transition_days: parseInt(transitionDays), 161 }; 162 163 rules = { 164 ...transition, 165 noncurrentversion_transition_days: parseInt(NCTransitionDays), 166 noncurrentversion_transition_storage_class: NCTransitionSC, 167 storage_class: storageClass, 168 }; 169 } 170 171 const lifecycleInsert = { 172 buckets, 173 type: ilmType, 174 prefix, 175 tags, 176 expired_object_delete_marker: expiredObjectDM, 177 expired_object_delete_all: expiredAllVersionsDM, 178 ...rules, 179 }; 180 181 api.buckets 182 .addMultiBucketLifecycle(lifecycleInsert) 183 .then((res) => { 184 setAddLoading(false); 185 setResults(res.data); 186 to("++"); 187 }) 188 .catch((err) => { 189 setAddLoading(false); 190 dispatch(setModalErrorSnackMessage(errorToHandler(err.error))); 191 }); 192 }; 193 194 return ( 195 <ModalWrapper 196 modalOpen={open} 197 onClose={() => { 198 closeModalAndRefresh(false); 199 }} 200 title="Set Lifecycle to multiple buckets" 201 > 202 <Wizard 203 loadingStep={addLoading || loadingTiers} 204 wizardSteps={[ 205 { 206 label: "Lifecycle Configuration", 207 componentRender: ( 208 <Fragment> 209 <FormLayout withBorders={false} containerPadding={false}> 210 <Grid item xs={12}> 211 <ReadBox 212 label="Local Buckets to replicate" 213 sx={{ maxWidth: "440px", width: "100%" }} 214 > 215 {buckets.join(", ")} 216 </ReadBox> 217 </Grid> 218 <h4>Remote Endpoint Configuration</h4> 219 <fieldset className={"inputItem"}> 220 <legend>Lifecycle Configuration</legend> 221 <RadioGroup 222 currentValue={ilmType} 223 id="quota_type" 224 name="quota_type" 225 label="ILM Rule" 226 onChange={(e: React.ChangeEvent<{ value: unknown }>) => { 227 setIlmType(e.target.value as "expiry" | "transition"); 228 }} 229 selectorOptions={[ 230 { value: "expiry", label: "Expiry" }, 231 { value: "transition", label: "Transition" }, 232 ]} 233 /> 234 {ilmType === "expiry" ? ( 235 <Fragment> 236 <InputBox 237 type="number" 238 id="expiry_days" 239 name="expiry_days" 240 onChange={( 241 e: React.ChangeEvent<HTMLInputElement>, 242 ) => { 243 setExpiryDays(e.target.value); 244 }} 245 label="Expiry Days" 246 value={expiryDays} 247 min="0" 248 /> 249 <InputBox 250 type="number" 251 id="noncurrentversion_expiration_days" 252 name="noncurrentversion_expiration_days" 253 onChange={( 254 e: React.ChangeEvent<HTMLInputElement>, 255 ) => { 256 setNCExpirationDays(e.target.value); 257 }} 258 label="Non-current Expiration Days" 259 value={NCExpirationDays} 260 min="0" 261 /> 262 </Fragment> 263 ) : ( 264 <Fragment> 265 <InputBox 266 type="number" 267 id="transition_days" 268 name="transition_days" 269 onChange={( 270 e: React.ChangeEvent<HTMLInputElement>, 271 ) => { 272 setTransitionDays(e.target.value); 273 }} 274 label="Transition Days" 275 value={transitionDays} 276 min="0" 277 /> 278 <InputBox 279 type="number" 280 id="noncurrentversion_transition_days" 281 name="noncurrentversion_transition_days" 282 onChange={( 283 e: React.ChangeEvent<HTMLInputElement>, 284 ) => { 285 setNCTransitionDays(e.target.value); 286 }} 287 label="Non-current Transition Days" 288 value={NCTransitionDays} 289 min="0" 290 /> 291 <InputBox 292 id="noncurrentversion_t_SC" 293 name="noncurrentversion_t_SC" 294 onChange={( 295 e: React.ChangeEvent<HTMLInputElement>, 296 ) => { 297 setNCTransitionSC(e.target.value); 298 }} 299 placeholder="Set Non-current Version Transition Storage Class" 300 label="Non-current Version Transition Storage Class" 301 value={NCTransitionSC} 302 /> 303 <Select 304 label="Storage Class" 305 id="storage_class" 306 name="storage_class" 307 value={storageClass} 308 onChange={(value) => { 309 setStorageClass(value); 310 }} 311 options={tiersList} 312 /> 313 </Fragment> 314 )} 315 </fieldset> 316 <fieldset className={"inputItem"}> 317 <legend>File Configuration</legend> 318 <InputBox 319 id="prefix" 320 name="prefix" 321 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 322 setPrefix(e.target.value); 323 }} 324 label="Prefix" 325 value={prefix} 326 /> 327 <QueryMultiSelector 328 name="tags" 329 label="Tags" 330 elements={tags} 331 onChange={(vl: string) => { 332 setTags(vl); 333 }} 334 keyPlaceholder="Tag Key" 335 valuePlaceholder="Tag Value" 336 withBorder 337 /> 338 <Switch 339 value="expired_delete_marker" 340 id="expired_delete_marker" 341 name="expired_delete_marker" 342 checked={expiredObjectDM} 343 onChange={( 344 event: React.ChangeEvent<HTMLInputElement>, 345 ) => { 346 setExpiredObjectDM(event.target.checked); 347 }} 348 label={"Expired Object Delete Marker"} 349 /> 350 <Switch 351 value="expired_delete_all" 352 id="expired_delete_all" 353 name="expired_delete_all" 354 checked={expiredAllVersionsDM} 355 onChange={( 356 event: React.ChangeEvent<HTMLInputElement>, 357 ) => { 358 setExpiredAllVersionsDM(event.target.checked); 359 }} 360 label={"Expired All Versions"} 361 /> 362 </fieldset> 363 </FormLayout> 364 </Fragment> 365 ), 366 buttons: [ 367 { 368 type: "custom", 369 label: "Create Rules", 370 enabled: !loadingTiers && !addLoading && isFormValid, 371 action: createLifecycleRules, 372 }, 373 ], 374 }, 375 { 376 label: "Results", 377 componentRender: ( 378 <Fragment> 379 <h3>Multi Bucket lifecycle Assignments Results</h3> 380 <Grid container> 381 <Grid item xs={12}> 382 <h4>Buckets Results</h4> 383 {results?.results?.map((resultItem) => { 384 return ( 385 <Box 386 sx={{ 387 display: "grid", 388 gridTemplateColumns: "45px auto", 389 alignItems: "center", 390 justifyContent: "stretch", 391 }} 392 > 393 {LogoToShow({ errString: resultItem.error || "" })} 394 <span>{resultItem.bucketName}</span> 395 </Box> 396 ); 397 })} 398 </Grid> 399 </Grid> 400 </Fragment> 401 ), 402 buttons: [ 403 { 404 type: "custom", 405 label: "Done", 406 enabled: !addLoading, 407 action: () => closeModalAndRefresh(true), 408 }, 409 ], 410 }, 411 ]} 412 forModal 413 /> 414 </ModalWrapper> 415 ); 416 }; 417 418 export default AddBulkReplicationModal;