github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/BucketDetails/AddLifecycleModal.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, { Fragment, useEffect, useState } from "react"; 18 19 import get from "lodash/get"; 20 import { 21 Accordion, 22 AlertIcon, 23 Button, 24 FormLayout, 25 Grid, 26 HelpTip, 27 InputBox, 28 LifecycleConfigIcon, 29 ProgressBar, 30 RadioGroup, 31 Select, 32 Switch, 33 } from "mds"; 34 import { useSelector } from "react-redux"; 35 import { api } from "api"; 36 import { BucketVersioningResponse, Tier } from "api/consoleApi"; 37 import { errorToHandler } from "api/errors"; 38 import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary"; 39 import { selDistSet, setModalErrorSnackMessage } from "../../../../systemSlice"; 40 import { useAppDispatch } from "../../../../store"; 41 import { ITiersDropDown } from "../types"; 42 import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper"; 43 import QueryMultiSelector from "../../Common/FormComponents/QueryMultiSelector/QueryMultiSelector"; 44 import InputUnitMenu from "../../Common/FormComponents/InputUnitMenu/InputUnitMenu"; 45 import { IAM_PAGES } from "common/SecureComponent/permissions"; 46 47 interface IReplicationModal { 48 open: boolean; 49 closeModalAndRefresh: (refresh: boolean) => any; 50 bucketName: string; 51 } 52 53 const AddLifecycleModal = ({ 54 open, 55 closeModalAndRefresh, 56 bucketName, 57 }: IReplicationModal) => { 58 const dispatch = useAppDispatch(); 59 const distributedSetup = useSelector(selDistSet); 60 const [loadingTiers, setLoadingTiers] = useState<boolean>(true); 61 const [tiersList, setTiersList] = useState<ITiersDropDown[]>([]); 62 const [addLoading, setAddLoading] = useState(false); 63 const [versioningInfo, setVersioningInfo] = 64 useState<BucketVersioningResponse | null>(null); 65 const [prefix, setPrefix] = useState(""); 66 const [tags, setTags] = useState<string>(""); 67 const [storageClass, setStorageClass] = useState(""); 68 69 const [ilmType, setIlmType] = useState<"expiry" | "transition">("expiry"); 70 const [targetVersion, setTargetVersion] = useState<"current" | "noncurrent">( 71 "current", 72 ); 73 const [lifecycleDays, setLifecycleDays] = useState<string>(""); 74 const [isFormValid, setIsFormValid] = useState<boolean>(false); 75 const [expiredObjectDM, setExpiredObjectDM] = useState<boolean>(false); 76 const [expiredAllVersionsDM, setExpiredAllVersionsDM] = 77 useState<boolean>(false); 78 const [loadingVersioning, setLoadingVersioning] = useState<boolean>(true); 79 const [expandedAdv, setExpandedAdv] = useState<boolean>(false); 80 const [expanded, setExpanded] = useState<boolean>(false); 81 const [expiryUnit, setExpiryUnit] = useState<string>("days"); 82 83 /*To be removed on component replacement*/ 84 const formFieldRowFilter = { 85 "& .MuiPaper-root": { padding: 0 }, 86 }; 87 88 useEffect(() => { 89 if (loadingTiers) { 90 api.admin 91 .tiersList() 92 .then((res) => { 93 const tiersList: Tier[] | null = get(res.data, "items", []); 94 95 if (tiersList !== null && tiersList.length >= 1) { 96 const objList = tiersList.map((tier: Tier) => { 97 const tierType = tier.type; 98 const value = get(tier, `${tierType}.name`, ""); 99 100 return { label: value, value: value }; 101 }); 102 103 setTiersList(objList); 104 if (objList.length > 0) { 105 setStorageClass(objList[0].value); 106 } 107 } 108 setLoadingTiers(false); 109 }) 110 .catch(() => { 111 setLoadingTiers(false); 112 }); 113 } 114 }, [loadingTiers]); 115 116 useEffect(() => { 117 let valid = true; 118 119 if (ilmType !== "expiry") { 120 if (storageClass === "") { 121 valid = false; 122 } 123 } 124 if (!lifecycleDays || parseInt(lifecycleDays) === 0) { 125 valid = false; 126 } 127 if (parseInt(lifecycleDays) > 2147483647) { 128 //values over int32 cannot be parsed 129 valid = false; 130 } 131 setIsFormValid(valid); 132 }, [ilmType, lifecycleDays, storageClass]); 133 134 useEffect(() => { 135 if (loadingVersioning && distributedSetup) { 136 api.buckets 137 .getBucketVersioning(bucketName) 138 .then((res) => { 139 setVersioningInfo(res.data); 140 setLoadingVersioning(false); 141 }) 142 .catch((err) => { 143 dispatch(setModalErrorSnackMessage(errorToHandler(err))); 144 setLoadingVersioning(false); 145 }); 146 } 147 }, [loadingVersioning, dispatch, bucketName, distributedSetup]); 148 149 const addRecord = () => { 150 let rules = {}; 151 152 if (ilmType === "expiry") { 153 let expiry: { [key: string]: number } = {}; 154 155 if (targetVersion === "current") { 156 expiry["expiry_days"] = parseInt(lifecycleDays); 157 } else if (expiryUnit === "days") { 158 expiry["noncurrentversion_expiration_days"] = parseInt(lifecycleDays); 159 } else { 160 expiry["newer_noncurrentversion_expiration_versions"] = 161 parseInt(lifecycleDays); 162 } 163 164 rules = { 165 ...expiry, 166 }; 167 } else { 168 let transition: { [key: string]: number | string } = {}; 169 if (targetVersion === "current") { 170 transition["transition_days"] = parseInt(lifecycleDays); 171 transition["storage_class"] = storageClass; 172 } else if (expiryUnit === "days") { 173 transition["noncurrentversion_transition_days"] = 174 parseInt(lifecycleDays); 175 transition["noncurrentversion_transition_storage_class"] = storageClass; 176 } 177 178 rules = { 179 ...transition, 180 }; 181 } 182 183 const lifecycleInsert = { 184 type: ilmType, 185 prefix, 186 tags, 187 expired_object_delete_marker: expiredObjectDM, 188 expired_object_delete_all: expiredAllVersionsDM, 189 ...rules, 190 }; 191 192 api.buckets 193 .addBucketLifecycle(bucketName, lifecycleInsert) 194 .then(() => { 195 setAddLoading(false); 196 closeModalAndRefresh(true); 197 }) 198 .catch((err) => { 199 setAddLoading(false); 200 dispatch(setModalErrorSnackMessage(errorToHandler(err))); 201 }); 202 }; 203 return ( 204 <ModalWrapper 205 modalOpen={open} 206 onClose={() => { 207 closeModalAndRefresh(false); 208 }} 209 title="Add Lifecycle Rule" 210 titleIcon={<LifecycleConfigIcon />} 211 > 212 {loadingTiers && ( 213 <Grid container> 214 <Grid item xs={12}> 215 <ProgressBar /> 216 </Grid> 217 </Grid> 218 )} 219 220 {!loadingTiers && ( 221 <form 222 noValidate 223 autoComplete="off" 224 onSubmit={(e: React.FormEvent<HTMLFormElement>) => { 225 e.preventDefault(); 226 setAddLoading(true); 227 addRecord(); 228 }} 229 > 230 <FormLayout withBorders={false} containerPadding={false}> 231 <RadioGroup 232 currentValue={ilmType} 233 id="ilm_type" 234 name="ilm_type" 235 label="Type of Lifecycle" 236 onChange={(e) => { 237 setIlmType(e.target.value as "expiry" | "transition"); 238 }} 239 selectorOptions={[ 240 { value: "expiry", label: "Expiry" }, 241 { value: "transition", label: "Transition" }, 242 ]} 243 helpTip={ 244 <Fragment> 245 Select{" "} 246 <a 247 target="blank" 248 href="https://min.io/docs/minio/kubernetes/upstream/administration/object-management/create-lifecycle-management-expiration-rule.html" 249 > 250 Expiry 251 </a>{" "} 252 to delete Objects per this rule. Select{" "} 253 <a 254 target="blank" 255 href="https://min.io/docs/minio/kubernetes/upstream/administration/object-management/transition-objects-to-minio.html" 256 > 257 Transition 258 </a>{" "} 259 to move Objects to a remote storage{" "} 260 <a 261 target="blank" 262 href="https://min.io/docs/minio/windows/administration/object-management/transition-objects-to-minio.html#configure-the-remote-storage-tier" 263 > 264 Tier 265 </a>{" "} 266 per this rule. 267 </Fragment> 268 } 269 helpTipPlacement="right" 270 /> 271 {versioningInfo?.status === "Enabled" && ( 272 <Select 273 value={targetVersion} 274 id="object_version" 275 name="object_version" 276 label="Object Version" 277 onChange={(value) => { 278 setTargetVersion(value as "current" | "noncurrent"); 279 }} 280 options={[ 281 { value: "current", label: "Current Version" }, 282 { value: "noncurrent", label: "Non-Current Version" }, 283 ]} 284 helpTip={ 285 <Fragment> 286 Select whether to apply the rule to current or non-current 287 Object 288 <a 289 target="blank" 290 href="https://min.io/docs/minio/kubernetes/upstream/administration/object-management/create-lifecycle-management-expiration-rule.html#expire-versioned-objects" 291 > 292 {" "} 293 Versions 294 </a> 295 </Fragment> 296 } 297 helpTipPlacement="right" 298 /> 299 )} 300 301 <InputBox 302 error={ 303 lifecycleDays && !isFormValid 304 ? parseInt(lifecycleDays) <= 0 305 ? `Number of ${expiryUnit} to retain must be greater than zero` 306 : parseInt(lifecycleDays) > 2147483647 307 ? `Number of ${expiryUnit} must be less than or equal to 2147483647` 308 : "" 309 : "" 310 } 311 id="expiry_days" 312 name="expiry_days" 313 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 314 if (e.target.validity.valid) { 315 setLifecycleDays(e.target.value); 316 } 317 }} 318 pattern={"[0-9]*"} 319 label="After" 320 value={lifecycleDays} 321 overlayObject={ 322 <Fragment> 323 <Grid container sx={{ justifyContent: "center" }}> 324 <InputUnitMenu 325 id={"expire-current-unit"} 326 unitSelected={expiryUnit} 327 unitsList={[ 328 { label: "Days", value: "days" }, 329 { label: "Versions", value: "versions" }, 330 ]} 331 disabled={ 332 targetVersion !== "noncurrent" || ilmType !== "expiry" 333 } 334 onUnitChange={(newValue) => { 335 setExpiryUnit(newValue); 336 }} 337 /> 338 {ilmType === "expiry" && targetVersion === "noncurrent" && ( 339 <HelpTip 340 content={ 341 <Fragment> 342 Select to set expiry by days or newer noncurrent 343 versions 344 </Fragment> 345 } 346 placement="right" 347 > 348 {" "} 349 <AlertIcon style={{ width: 15, height: 15 }} /> 350 </HelpTip> 351 )} 352 </Grid> 353 </Fragment> 354 } 355 /> 356 357 {ilmType === "expiry" ? ( 358 <Fragment /> 359 ) : ( 360 <Select 361 label="To Tier" 362 id="storage_class" 363 name="storage_class" 364 value={storageClass} 365 onChange={(value) => { 366 setStorageClass(value as string); 367 }} 368 options={tiersList} 369 helpTip={ 370 <Fragment> 371 Configure a{" "} 372 <a 373 href={IAM_PAGES.TIERS_ADD} 374 color="secondary" 375 style={{ textDecoration: "underline" }} 376 > 377 remote tier 378 </a>{" "} 379 to receive transitioned Objects 380 </Fragment> 381 } 382 helpTipPlacement="right" 383 /> 384 )} 385 <Grid item xs={12} sx={formFieldRowFilter}> 386 <Accordion 387 title={"Filters"} 388 id={"lifecycle-filters"} 389 expanded={expanded} 390 onTitleClick={() => setExpanded(!expanded)} 391 > 392 <Grid item xs={12}> 393 <InputBox 394 id="prefix" 395 name="prefix" 396 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 397 setPrefix(e.target.value); 398 }} 399 label="Prefix" 400 value={prefix} 401 /> 402 </Grid> 403 <Grid item xs={12}> 404 <QueryMultiSelector 405 name="tags" 406 label="Tags" 407 elements={""} 408 onChange={(vl: string) => { 409 setTags(vl); 410 }} 411 keyPlaceholder="Tag Key" 412 valuePlaceholder="Tag Value" 413 withBorder 414 /> 415 </Grid> 416 </Accordion> 417 </Grid> 418 {ilmType === "expiry" && targetVersion === "noncurrent" && ( 419 <Grid item xs={12} sx={formFieldRowFilter}> 420 <Accordion 421 title={"Advanced"} 422 id={"lifecycle-advanced-filters"} 423 expanded={expandedAdv} 424 onTitleClick={() => setExpandedAdv(!expandedAdv)} 425 sx={{ marginTop: 15 }} 426 > 427 <Grid item xs={12}> 428 <Switch 429 value="expired_delete_marker" 430 id="expired_delete_marker" 431 name="expired_delete_marker" 432 checked={expiredObjectDM} 433 onChange={( 434 event: React.ChangeEvent<HTMLInputElement>, 435 ) => { 436 setExpiredObjectDM(event.target.checked); 437 }} 438 label={"Expire Delete Marker"} 439 description={ 440 "Remove the reference to the object if no versions are left" 441 } 442 /> 443 <Switch 444 value="expired_delete_all" 445 id="expired_delete_all" 446 name="expired_delete_all" 447 checked={expiredAllVersionsDM} 448 onChange={( 449 event: React.ChangeEvent<HTMLInputElement>, 450 ) => { 451 setExpiredAllVersionsDM(event.target.checked); 452 }} 453 label={"Expire All Versions"} 454 description={ 455 "Removes all the versions of the object already expired" 456 } 457 /> 458 </Grid> 459 </Accordion> 460 </Grid> 461 )} 462 463 <Grid item xs={12} sx={modalStyleUtils.modalButtonBar}> 464 <Button 465 id={"reset"} 466 type="button" 467 variant="regular" 468 disabled={addLoading} 469 onClick={() => { 470 closeModalAndRefresh(false); 471 }} 472 label={"Cancel"} 473 /> 474 <Button 475 id={"save-lifecycle"} 476 type="submit" 477 variant="callAction" 478 color="primary" 479 disabled={addLoading || !isFormValid} 480 label={"Save"} 481 /> 482 </Grid> 483 {addLoading && ( 484 <Grid item xs={12}> 485 <ProgressBar /> 486 </Grid> 487 )} 488 </FormLayout> 489 </form> 490 )} 491 </ModalWrapper> 492 ); 493 }; 494 495 export default AddLifecycleModal;