github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/BucketDetails/EditLifecycleConfiguration.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 import get from "lodash/get"; 19 import { 20 Accordion, 21 Button, 22 FormLayout, 23 Grid, 24 InputBox, 25 LifecycleConfigIcon, 26 Loader, 27 ProgressBar, 28 RadioGroup, 29 Select, 30 Switch, 31 } from "mds"; 32 import { api } from "api"; 33 import { ApiError, Tier } from "api/consoleApi"; 34 import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary"; 35 import { ITiersDropDown, LifeCycleItem } from "../types"; 36 import { setErrorSnackMessage } from "../../../../systemSlice"; 37 import { useAppDispatch } from "../../../../store"; 38 import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper"; 39 import QueryMultiSelector from "../../Common/FormComponents/QueryMultiSelector/QueryMultiSelector"; 40 import { errorToHandler } from "../../../../api/errors"; 41 42 interface IAddUserContentProps { 43 closeModalAndRefresh: (reload: boolean) => void; 44 selectedBucket: string; 45 lifecycleRule: LifeCycleItem; 46 open: boolean; 47 } 48 49 const EditLifecycleConfiguration = ({ 50 closeModalAndRefresh, 51 selectedBucket, 52 lifecycleRule, 53 open, 54 }: IAddUserContentProps) => { 55 const dispatch = useAppDispatch(); 56 const [loadingTiers, setLoadingTiers] = useState<boolean>(true); 57 const [addLoading, setAddLoading] = useState<boolean>(false); 58 const [tags, setTags] = useState<string>(""); 59 const [enabled, setEnabled] = useState<boolean>(false); 60 const [tiersList, setTiersList] = useState<ITiersDropDown[]>([]); 61 const [prefix, setPrefix] = useState(""); 62 const [storageClass, setStorageClass] = useState(""); 63 const [NCTransitionSC, setNCTransitionSC] = useState(""); 64 const [expiredObjectDM, setExpiredObjectDM] = useState<boolean>(false); 65 const [expiredAllVersionsDM, setExpiredAllVersionsDM] = 66 useState<boolean>(false); 67 const [NCExpirationDays, setNCExpirationDays] = useState<string>("0"); 68 const [NCTransitionDays, setNCTransitionDays] = useState<string>("0"); 69 const [ilmType, setIlmType] = useState<"transition" | "expiry">("expiry"); 70 const [expiryDays, setExpiryDays] = useState<string>("0"); 71 const [transitionDays, setTransitionDays] = useState<string>("0"); 72 const [isFormValid, setIsFormValid] = useState<boolean>(false); 73 const [expandedAdv, setExpandedAdv] = useState<boolean>(false); 74 const [expanded, setExpanded] = useState<boolean>(false); 75 76 const ILM_TYPES = [ 77 { value: "expiry", label: "Expiry" }, 78 { value: "transition", label: "Transition" }, 79 ]; 80 81 useEffect(() => { 82 if (loadingTiers) { 83 api.admin 84 .tiersList() 85 .then((res) => { 86 const tiersList: Tier[] | null = get(res.data, "items", []); 87 88 if (tiersList !== null && tiersList.length >= 1) { 89 const objList = tiersList.map((tier: Tier) => { 90 const tierType = tier.type; 91 const value = get(tier, `${tierType}.name`, ""); 92 93 return { label: value, value: value }; 94 }); 95 96 setTiersList(objList); 97 if (objList.length > 0) { 98 setStorageClass(objList[0].value); 99 } 100 } 101 setLoadingTiers(false); 102 }) 103 .catch(() => { 104 setLoadingTiers(false); 105 }); 106 } 107 }, [loadingTiers]); 108 109 useEffect(() => { 110 let valid = true; 111 112 if (ilmType !== "expiry") { 113 if (storageClass === "") { 114 valid = false; 115 } 116 } 117 setIsFormValid(valid); 118 }, [ilmType, expiryDays, transitionDays, storageClass]); 119 120 useEffect(() => { 121 if (lifecycleRule.status === "Enabled") { 122 setEnabled(true); 123 } 124 125 let transitionMode = false; 126 127 if (lifecycleRule.transition) { 128 if ( 129 lifecycleRule.transition.days && 130 lifecycleRule.transition.days !== 0 131 ) { 132 setTransitionDays(lifecycleRule.transition.days.toString()); 133 setIlmType("transition"); 134 transitionMode = true; 135 } 136 if ( 137 lifecycleRule.transition.noncurrent_transition_days && 138 lifecycleRule.transition.noncurrent_transition_days !== 0 139 ) { 140 setNCTransitionDays( 141 lifecycleRule.transition.noncurrent_transition_days.toString(), 142 ); 143 setIlmType("transition"); 144 transitionMode = true; 145 } 146 147 // Fallback to old rules by date 148 if ( 149 lifecycleRule.transition.date && 150 lifecycleRule.transition.date !== "0001-01-01T00:00:00Z" 151 ) { 152 setIlmType("transition"); 153 transitionMode = true; 154 } 155 } 156 157 if (lifecycleRule.expiration) { 158 if ( 159 lifecycleRule.expiration.days && 160 lifecycleRule.expiration.days !== 0 161 ) { 162 setExpiryDays(lifecycleRule.expiration.days.toString()); 163 setIlmType("expiry"); 164 transitionMode = false; 165 } 166 if ( 167 lifecycleRule.expiration.noncurrent_expiration_days && 168 lifecycleRule.expiration.noncurrent_expiration_days !== 0 169 ) { 170 setNCExpirationDays( 171 lifecycleRule.expiration.noncurrent_expiration_days.toString(), 172 ); 173 setIlmType("expiry"); 174 transitionMode = false; 175 } 176 177 // Fallback to old rules by date 178 if ( 179 lifecycleRule.expiration.date && 180 lifecycleRule.expiration.date !== "0001-01-01T00:00:00Z" 181 ) { 182 setIlmType("expiry"); 183 transitionMode = false; 184 } 185 } 186 187 // Transition fields 188 if (transitionMode) { 189 setStorageClass(lifecycleRule.transition?.storage_class || ""); 190 setNCTransitionDays( 191 lifecycleRule.transition?.noncurrent_transition_days?.toString() || "0", 192 ); 193 setNCTransitionSC( 194 lifecycleRule.transition?.noncurrent_storage_class || "", 195 ); 196 } else { 197 // Expiry fields 198 setNCExpirationDays( 199 lifecycleRule.expiration?.noncurrent_expiration_days?.toString() || "0", 200 ); 201 } 202 203 setExpiredObjectDM(!!lifecycleRule.expiration?.delete_marker); 204 setExpiredAllVersionsDM(!!lifecycleRule.expiration?.delete_all); 205 setPrefix(lifecycleRule.prefix || ""); 206 207 if (lifecycleRule.tags) { 208 const tgs = lifecycleRule.tags.reduce( 209 (stringLab: string, currItem: any, index: number) => { 210 return `${stringLab}${index !== 0 ? "&" : ""}${currItem.key}=${ 211 currItem.value 212 }`; 213 }, 214 "", 215 ); 216 217 setTags(tgs); 218 } 219 }, [lifecycleRule]); 220 221 const saveRecord = (event: React.FormEvent) => { 222 event.preventDefault(); 223 224 if (addLoading) { 225 return; 226 } 227 setAddLoading(true); 228 if (selectedBucket !== null && lifecycleRule !== null) { 229 let rules = {}; 230 231 if (ilmType === "expiry") { 232 let expiry: { [key: string]: number } = {}; 233 234 if ( 235 lifecycleRule.expiration?.days && 236 lifecycleRule.expiration?.days > 0 237 ) { 238 expiry["expiry_days"] = parseInt(expiryDays); 239 } 240 if (lifecycleRule.expiration?.noncurrent_expiration_days) { 241 expiry["noncurrentversion_expiration_days"] = 242 parseInt(NCExpirationDays); 243 } 244 245 rules = { 246 ...expiry, 247 }; 248 } else { 249 let transition: { [key: string]: number | string } = {}; 250 251 if ( 252 lifecycleRule.transition?.days && 253 lifecycleRule.transition?.days > 0 254 ) { 255 transition["transition_days"] = parseInt(transitionDays); 256 transition["storage_class"] = storageClass; 257 } 258 if (lifecycleRule.transition?.noncurrent_transition_days) { 259 transition["noncurrentversion_transition_days"] = 260 parseInt(NCTransitionDays); 261 transition["noncurrentversion_transition_storage_class"] = 262 NCTransitionSC; 263 } 264 265 rules = { 266 ...transition, 267 }; 268 } 269 270 const lifecycleUpdate = { 271 type: ilmType, 272 disable: !enabled, 273 prefix, 274 tags, 275 expired_object_delete_marker: expiredObjectDM, 276 expired_object_delete_all: expiredAllVersionsDM, 277 ...rules, 278 }; 279 280 api.buckets 281 .updateBucketLifecycle( 282 selectedBucket, 283 lifecycleRule.id, 284 lifecycleUpdate, 285 ) 286 .then((res) => { 287 setAddLoading(false); 288 closeModalAndRefresh(true); 289 }) 290 .catch(async (eRes) => { 291 setAddLoading(false); 292 const err = (await eRes.json()) as ApiError; 293 dispatch(setErrorSnackMessage(errorToHandler(err))); 294 }); 295 } 296 }; 297 298 let objectVersion = ""; 299 300 if (lifecycleRule.expiration) { 301 if (lifecycleRule.expiration.days > 0) { 302 objectVersion = "Current Version"; 303 } else if (lifecycleRule.expiration.noncurrent_expiration_days) { 304 objectVersion = "Non-Current Version"; 305 } 306 } 307 308 if (lifecycleRule.transition) { 309 if (lifecycleRule.transition.days > 0) { 310 objectVersion = "Current Version"; 311 } else if (lifecycleRule.transition.noncurrent_transition_days) { 312 objectVersion = "Non-Current Version"; 313 } 314 } 315 316 return ( 317 <ModalWrapper 318 onClose={() => { 319 closeModalAndRefresh(false); 320 }} 321 modalOpen={open} 322 title={"Edit Lifecycle Configuration"} 323 titleIcon={<LifecycleConfigIcon />} 324 > 325 {!loadingTiers ? ( 326 <form 327 noValidate 328 autoComplete="off" 329 onSubmit={(e: React.FormEvent<HTMLFormElement>) => { 330 saveRecord(e); 331 }} 332 > 333 <FormLayout containerPadding={false} withBorders={false}> 334 <Switch 335 label="Status" 336 indicatorLabels={["Enabled", "Disabled"]} 337 checked={enabled} 338 value={"user_enabled"} 339 id="rule_status" 340 name="rule_status" 341 onChange={(e) => { 342 setEnabled(e.target.checked); 343 }} 344 /> 345 <InputBox 346 id="id" 347 name="id" 348 label="Id" 349 value={lifecycleRule.id} 350 onChange={() => {}} 351 disabled 352 /> 353 {ilmType ? ( 354 <RadioGroup 355 currentValue={ilmType} 356 id="rule_type" 357 name="rule_type" 358 label="Rule Type" 359 selectorOptions={ILM_TYPES} 360 onChange={() => {}} 361 disableOptions 362 /> 363 ) : null} 364 365 <InputBox 366 id="object-version" 367 name="object-version" 368 label="Object Version" 369 value={objectVersion} 370 onChange={() => {}} 371 disabled 372 /> 373 374 {ilmType === "expiry" && lifecycleRule.expiration?.days && ( 375 <InputBox 376 type="number" 377 id="expiry_days" 378 name="expiry_days" 379 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 380 setExpiryDays(e.target.value); 381 }} 382 label="Expiry Days" 383 value={expiryDays} 384 min="0" 385 /> 386 )} 387 388 {ilmType === "expiry" && 389 lifecycleRule.expiration?.noncurrent_expiration_days && ( 390 <InputBox 391 type="number" 392 id="noncurrentversion_expiration_days" 393 name="noncurrentversion_expiration_days" 394 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 395 setNCExpirationDays(e.target.value); 396 }} 397 label="Non-current Expiration Days" 398 value={NCExpirationDays} 399 min="0" 400 /> 401 )} 402 {ilmType === "transition" && lifecycleRule.transition?.days && ( 403 <Fragment> 404 <InputBox 405 type="number" 406 id="transition_days" 407 name="transition_days" 408 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 409 setTransitionDays(e.target.value); 410 }} 411 label="Transition Days" 412 value={transitionDays} 413 min="0" 414 /> 415 <Select 416 label="Tier" 417 id="storage_class" 418 name="storage_class" 419 value={storageClass} 420 onChange={(value) => { 421 setStorageClass(value); 422 }} 423 options={tiersList} 424 /> 425 </Fragment> 426 )} 427 428 {ilmType === "transition" && 429 lifecycleRule.transition?.noncurrent_transition_days && ( 430 <Fragment> 431 <InputBox 432 type="number" 433 id="noncurrentversion_transition_days" 434 name="noncurrentversion_transition_days" 435 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 436 setNCTransitionDays(e.target.value); 437 }} 438 label="Non-current Transition Days" 439 value={NCTransitionDays} 440 min="0" 441 /> 442 <InputBox 443 id="noncurrentversion_t_SC" 444 name="noncurrentversion_t_SC" 445 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 446 setNCTransitionSC(e.target.value); 447 }} 448 placeholder="Set Non-current Version Transition Storage Class" 449 label="Non-current Version Transition Storage Class" 450 value={NCTransitionSC} 451 /> 452 </Fragment> 453 )} 454 <Grid item xs={12}> 455 <Accordion 456 title={"Filters"} 457 id={"lifecycle-filters"} 458 expanded={expanded} 459 onTitleClick={() => setExpanded(!expanded)} 460 > 461 <InputBox 462 id="prefix" 463 name="prefix" 464 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 465 setPrefix(e.target.value); 466 }} 467 label="Prefix" 468 value={prefix} 469 /> 470 <QueryMultiSelector 471 name="tags" 472 label="Tags" 473 elements={tags} 474 onChange={(vl: string) => { 475 setTags(vl); 476 }} 477 keyPlaceholder="Tag Key" 478 valuePlaceholder="Tag Value" 479 withBorder 480 /> 481 </Accordion> 482 </Grid> 483 {ilmType === "expiry" && 484 lifecycleRule.expiration?.noncurrent_expiration_days && ( 485 <Grid item xs={12}> 486 <Accordion 487 title={"Advanced"} 488 id={"lifecycle-advanced-filters"} 489 expanded={expandedAdv} 490 onTitleClick={() => setExpandedAdv(!expandedAdv)} 491 sx={{ marginTop: 15 }} 492 > 493 <Switch 494 value="expired_delete_marker" 495 id="expired_delete_marker" 496 name="expired_delete_marker" 497 checked={expiredObjectDM} 498 onChange={( 499 event: React.ChangeEvent<HTMLInputElement>, 500 ) => { 501 setExpiredObjectDM(event.target.checked); 502 }} 503 label={"Expired Object Delete Marker"} 504 /> 505 <Switch 506 value="expired_delete_all" 507 id="expired_delete_all" 508 name="expired_delete_all" 509 checked={expiredAllVersionsDM} 510 onChange={( 511 event: React.ChangeEvent<HTMLInputElement>, 512 ) => { 513 setExpiredAllVersionsDM(event.target.checked); 514 }} 515 label={"Expired All Versions"} 516 /> 517 </Accordion> 518 </Grid> 519 )} 520 <Grid item xs={12} sx={modalStyleUtils.modalButtonBar}> 521 <Button 522 id={"cancel"} 523 type="button" 524 variant="regular" 525 disabled={addLoading} 526 onClick={() => { 527 closeModalAndRefresh(false); 528 }} 529 label={"Cancel"} 530 /> 531 <Button 532 id={"save"} 533 type="submit" 534 variant="callAction" 535 color="primary" 536 disabled={addLoading || !isFormValid} 537 label={"Save"} 538 /> 539 </Grid> 540 {addLoading && ( 541 <Grid item xs={12}> 542 <ProgressBar /> 543 </Grid> 544 )} 545 </FormLayout> 546 </form> 547 ) : ( 548 <Loader style={{ width: 16, height: 16 }} /> 549 )} 550 </ModalWrapper> 551 ); 552 }; 553 554 export default EditLifecycleConfiguration;