github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/ListBuckets/AddBucket/AddBucket.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 styled from "styled-components"; 19 import get from "lodash/get"; 20 21 import { useNavigate } from "react-router-dom"; 22 import { 23 BackLink, 24 Box, 25 BucketsIcon, 26 Button, 27 FormLayout, 28 Grid, 29 HelpBox, 30 InfoIcon, 31 InputBox, 32 PageLayout, 33 RadioGroup, 34 Switch, 35 SectionTitle, 36 ProgressBar, 37 } from "mds"; 38 import { k8sScalarUnitsExcluding } from "../../../../../common/utils"; 39 import { AppState, useAppDispatch } from "../../../../../store"; 40 import { useSelector } from "react-redux"; 41 import { 42 selDistSet, 43 selSiteRep, 44 setErrorSnackMessage, 45 setHelpName, 46 } from "../../../../../systemSlice"; 47 import InputUnitMenu from "../../../Common/FormComponents/InputUnitMenu/InputUnitMenu"; 48 import TooltipWrapper from "../../../Common/TooltipWrapper/TooltipWrapper"; 49 import { 50 resetForm, 51 setEnableObjectLocking, 52 setExcludedPrefixes, 53 setExcludeFolders, 54 setIsDirty, 55 setName, 56 setQuota, 57 setQuotaSize, 58 setQuotaUnit, 59 setRetention, 60 setRetentionMode, 61 setRetentionUnit, 62 setRetentionValidity, 63 setVersioning, 64 } from "./addBucketsSlice"; 65 import { addBucketAsync } from "./addBucketThunks"; 66 import AddBucketName from "./AddBucketName"; 67 import { 68 IAM_SCOPES, 69 permissionTooltipHelper, 70 } from "../../../../../common/SecureComponent/permissions"; 71 import { hasPermission } from "../../../../../common/SecureComponent"; 72 import BucketNamingRules from "./BucketNamingRules"; 73 import PageHeaderWrapper from "../../../Common/PageHeaderWrapper/PageHeaderWrapper"; 74 import { api } from "../../../../../api"; 75 import { ObjectRetentionMode } from "../../../../../api/consoleApi"; 76 import { errorToHandler } from "../../../../../api/errors"; 77 import HelpMenu from "../../../HelpMenu"; 78 import CSVMultiSelector from "../../../Common/FormComponents/CSVMultiSelector/CSVMultiSelector"; 79 80 const ErrorBox = styled.div(({ theme }) => ({ 81 color: get(theme, "signalColors.danger", "#C51B3F"), 82 border: `1px solid ${get(theme, "signalColors.danger", "#C51B3F")}`, 83 padding: 8, 84 borderRadius: 3, 85 })); 86 87 const AddBucket = () => { 88 const dispatch = useAppDispatch(); 89 const navigate = useNavigate(); 90 91 const validBucketCharacters = new RegExp( 92 `^[a-z0-9][a-z0-9\\.\\-]{1,61}[a-z0-9]$`, 93 ); 94 const ipAddressFormat = new RegExp(`^(\\d+\\.){3}\\d+$`); 95 const bucketName = useSelector((state: AppState) => state.addBucket.name); 96 const isDirty = useSelector((state: AppState) => state.addBucket.isDirty); 97 const [validationResult, setValidationResult] = useState<boolean[]>([]); 98 const errorList = validationResult.filter((v) => !v); 99 const hasErrors = errorList.length > 0; 100 const [records, setRecords] = useState<string[]>([]); 101 const versioningEnabled = useSelector( 102 (state: AppState) => state.addBucket.versioningEnabled, 103 ); 104 const excludeFolders = useSelector( 105 (state: AppState) => state.addBucket.excludeFolders, 106 ); 107 const excludedPrefixes = useSelector( 108 (state: AppState) => state.addBucket.excludedPrefixes, 109 ); 110 const lockingEnabled = useSelector( 111 (state: AppState) => state.addBucket.lockingEnabled, 112 ); 113 const quotaEnabled = useSelector( 114 (state: AppState) => state.addBucket.quotaEnabled, 115 ); 116 const quotaSize = useSelector((state: AppState) => state.addBucket.quotaSize); 117 const quotaUnit = useSelector((state: AppState) => state.addBucket.quotaUnit); 118 const retentionEnabled = useSelector( 119 (state: AppState) => state.addBucket.retentionEnabled, 120 ); 121 const retentionMode = useSelector( 122 (state: AppState) => state.addBucket.retentionMode, 123 ); 124 const retentionUnit = useSelector( 125 (state: AppState) => state.addBucket.retentionUnit, 126 ); 127 const retentionValidity = useSelector( 128 (state: AppState) => state.addBucket.retentionValidity, 129 ); 130 const addLoading = useSelector((state: AppState) => state.addBucket.loading); 131 const invalidFields = useSelector( 132 (state: AppState) => state.addBucket.invalidFields, 133 ); 134 const lockingFieldDisabled = useSelector( 135 (state: AppState) => state.addBucket.lockingFieldDisabled, 136 ); 137 const distributedSetup = useSelector(selDistSet); 138 const siteReplicationInfo = useSelector(selSiteRep); 139 const navigateTo = useSelector( 140 (state: AppState) => state.addBucket.navigateTo, 141 ); 142 143 const lockingAllowed = hasPermission( 144 "*", 145 [ 146 IAM_SCOPES.S3_PUT_BUCKET_VERSIONING, 147 IAM_SCOPES.S3_PUT_BUCKET_OBJECT_LOCK_CONFIGURATION, 148 IAM_SCOPES.S3_PUT_ACTIONS, 149 ], 150 true, 151 ); 152 153 const versioningAllowed = hasPermission("*", [ 154 IAM_SCOPES.S3_PUT_BUCKET_VERSIONING, 155 IAM_SCOPES.S3_PUT_ACTIONS, 156 ]); 157 158 useEffect(() => { 159 const bucketNameErrors = [ 160 !(isDirty && (bucketName.length < 3 || bucketName.length > 63)), 161 validBucketCharacters.test(bucketName), 162 !( 163 bucketName.includes(".-") || 164 bucketName.includes("-.") || 165 bucketName.includes("..") 166 ), 167 !ipAddressFormat.test(bucketName), 168 !bucketName.startsWith("xn--"), 169 !bucketName.endsWith("-s3alias"), 170 !records.includes(bucketName), 171 ]; 172 setValidationResult(bucketNameErrors); 173 // eslint-disable-next-line react-hooks/exhaustive-deps 174 }, [bucketName, isDirty]); 175 176 useEffect(() => { 177 dispatch(setName("")); 178 dispatch(setIsDirty(false)); 179 const fetchRecords = () => { 180 api.buckets 181 .listBuckets() 182 .then((res) => { 183 if (res.data) { 184 var bucketList: string[] = []; 185 if (res.data.buckets != null && res.data.buckets.length > 0) { 186 res.data.buckets.forEach((bucket) => { 187 bucketList.push(bucket.name); 188 }); 189 } 190 setRecords(bucketList); 191 } else if (res.error) { 192 dispatch(setErrorSnackMessage(errorToHandler(res.error))); 193 } 194 }) 195 .catch((err) => { 196 dispatch(setErrorSnackMessage(errorToHandler(err))); 197 }); 198 }; 199 fetchRecords(); 200 }, [dispatch]); 201 202 const resForm = () => { 203 dispatch(resetForm()); 204 }; 205 206 useEffect(() => { 207 if (navigateTo !== "") { 208 const goTo = `${navigateTo}`; 209 dispatch(resetForm()); 210 navigate(goTo); 211 } 212 }, [navigateTo, navigate, dispatch]); 213 214 useEffect(() => { 215 dispatch(setHelpName("add_bucket")); 216 // eslint-disable-next-line react-hooks/exhaustive-deps 217 }, []); 218 219 return ( 220 <Fragment> 221 <PageHeaderWrapper 222 label={ 223 <BackLink label={"Buckets"} onClick={() => navigate("/buckets")} /> 224 } 225 actions={<HelpMenu />} 226 /> 227 <PageLayout> 228 <FormLayout 229 title={"Create Bucket"} 230 icon={<BucketsIcon />} 231 helpBox={ 232 <HelpBox 233 iconComponent={<BucketsIcon />} 234 title={"Buckets"} 235 help={ 236 <Fragment> 237 MinIO uses buckets to organize objects. A bucket is similar to 238 a folder or directory in a filesystem, where each bucket can 239 hold an arbitrary number of objects. 240 <br /> 241 <br /> 242 <b>Versioning</b> allows to keep multiple versions of the same 243 object under the same key. 244 <br /> 245 <br /> 246 <b>Object Locking</b> prevents objects from being deleted. 247 Required to support retention and legal hold. Can only be 248 enabled at bucket creation. 249 <br /> 250 <br /> 251 <b>Quota</b> limits the amount of data in the bucket. 252 {lockingAllowed && ( 253 <Fragment> 254 <br /> 255 <br /> 256 <b>Retention</b> imposes rules to prevent object deletion 257 for a period of time. Versioning must be enabled in order 258 to set bucket retention policies. 259 </Fragment> 260 )} 261 <br /> 262 <br /> 263 </Fragment> 264 } 265 /> 266 } 267 > 268 <form 269 noValidate 270 autoComplete="off" 271 onSubmit={(e: React.FormEvent<HTMLFormElement>) => { 272 e.preventDefault(); 273 dispatch(addBucketAsync()); 274 }} 275 > 276 <Box> 277 <AddBucketName hasErrors={hasErrors} /> 278 <Box sx={{ margin: "10px 0" }}> 279 <BucketNamingRules errorList={validationResult} /> 280 </Box> 281 <SectionTitle separator>Features</SectionTitle> 282 <Box sx={{ marginTop: 10 }}> 283 {!distributedSetup && ( 284 <Fragment> 285 <ErrorBox> 286 These features are unavailable in a single-disk setup. 287 <br /> 288 Please deploy a server in{" "} 289 <a 290 href="https://min.io/docs/minio/linux/operations/install-deploy-manage/deploy-minio-multi-node-multi-drive.html?ref=con" 291 target="_blank" 292 rel="noopener" 293 > 294 Distributed Mode 295 </a>{" "} 296 to use these features. 297 </ErrorBox> 298 <br /> 299 <br /> 300 </Fragment> 301 )} 302 303 {siteReplicationInfo.enabled && ( 304 <Fragment> 305 <br /> 306 <Box 307 withBorders 308 sx={{ 309 display: "flex", 310 alignItems: "center", 311 padding: "10px", 312 "& > .min-icon ": { 313 width: 20, 314 height: 20, 315 marginRight: 10, 316 }, 317 }} 318 > 319 <InfoIcon /> Versioning setting cannot be changed as 320 cluster replication is enabled for this site. 321 </Box> 322 <br /> 323 </Fragment> 324 )} 325 <Switch 326 value="versioned" 327 id="versioned" 328 name="versioned" 329 checked={versioningEnabled} 330 onChange={(event: React.ChangeEvent<HTMLInputElement>) => { 331 dispatch(setVersioning(event.target.checked)); 332 }} 333 label={"Versioning"} 334 disabled={ 335 !distributedSetup || 336 lockingEnabled || 337 siteReplicationInfo.enabled || 338 !versioningAllowed 339 } 340 tooltip={ 341 versioningAllowed 342 ? "" 343 : permissionTooltipHelper( 344 [ 345 IAM_SCOPES.S3_PUT_BUCKET_VERSIONING, 346 IAM_SCOPES.S3_PUT_ACTIONS, 347 ], 348 "Versioning", 349 ) 350 } 351 helpTip={ 352 <Fragment> 353 {lockingEnabled && versioningEnabled && ( 354 <strong> 355 {" "} 356 You must disable Object Locking before Versioning can 357 be disabled <br /> 358 </strong> 359 )} 360 MinIO supports keeping multiple{" "} 361 <a 362 href="https://min.io/docs/minio/kubernetes/upstream/administration/object-management/object-versioning.html#minio-bucket-versioning" 363 target="blank" 364 > 365 versions 366 </a>{" "} 367 of an object in a single bucket. 368 <br /> 369 Versioning is required to enable{" "} 370 <a 371 href="https://min.io/docs/minio/macos/administration/object-management.html#object-retention" 372 target="blank" 373 > 374 Object Locking 375 </a>{" "} 376 and{" "} 377 <a 378 href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#object-retention-modes" 379 target="blank" 380 > 381 Retention 382 </a> 383 . 384 </Fragment> 385 } 386 helpTipPlacement="right" 387 /> 388 {versioningEnabled && distributedSetup && !lockingEnabled && ( 389 <Fragment> 390 <Switch 391 id={"excludeFolders"} 392 label={"Exclude Folders"} 393 checked={excludeFolders} 394 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 395 dispatch(setExcludeFolders(e.target.checked)); 396 }} 397 indicatorLabels={["Enabled", "Disabled"]} 398 helpTip={ 399 <Fragment> 400 You can choose to{" "} 401 <a href="https://min.io/docs/minio/windows/administration/object-management/object-versioning.html#exclude-folders-from-versioning"> 402 exclude folders and prefixes 403 </a>{" "} 404 from versioning if Object Locking is not enabled. 405 <br /> 406 MinIO requires versioning to support replication. 407 <br /> 408 Objects in excluded prefixes do not replicate to any 409 peer site or remote site. 410 </Fragment> 411 } 412 helpTipPlacement="right" 413 /> 414 <CSVMultiSelector 415 elements={excludedPrefixes} 416 label={"Excluded Prefixes"} 417 name={"excludedPrefixes"} 418 onChange={(value: string | string[]) => { 419 let valCh = ""; 420 421 if (Array.isArray(value)) { 422 valCh = value.join(","); 423 } else { 424 valCh = value; 425 } 426 dispatch(setExcludedPrefixes(valCh)); 427 }} 428 withBorder={true} 429 /> 430 </Fragment> 431 )} 432 <Switch 433 value="locking" 434 id="locking" 435 name="locking" 436 disabled={ 437 lockingFieldDisabled || !distributedSetup || !lockingAllowed 438 } 439 checked={lockingEnabled} 440 onChange={(event: React.ChangeEvent<HTMLInputElement>) => { 441 dispatch(setEnableObjectLocking(event.target.checked)); 442 if (event.target.checked && !siteReplicationInfo.enabled) { 443 dispatch(setVersioning(true)); 444 } 445 }} 446 label={"Object Locking"} 447 tooltip={ 448 lockingAllowed 449 ? `` 450 : permissionTooltipHelper( 451 [ 452 IAM_SCOPES.S3_PUT_BUCKET_VERSIONING, 453 IAM_SCOPES.S3_PUT_BUCKET_OBJECT_LOCK_CONFIGURATION, 454 IAM_SCOPES.S3_PUT_ACTIONS, 455 ], 456 "Locking", 457 ) 458 } 459 helpTip={ 460 <Fragment> 461 {retentionEnabled && ( 462 <strong> 463 {" "} 464 You must disable Retention before Object Locking can 465 be disabled <br /> 466 </strong> 467 )} 468 You can only enable{" "} 469 <a 470 href="https://min.io/docs/minio/macos/administration/object-management.html#object-retention" 471 target="blank" 472 > 473 Object Locking 474 </a>{" "} 475 when first creating a bucket. 476 <br /> 477 <br /> 478 <a href="https://min.io/docs/minio/windows/administration/object-management/object-versioning.html#exclude-folders-from-versioning"> 479 Exclude folders and prefixes 480 </a>{" "} 481 options will not be available if this option is enabled. 482 </Fragment> 483 } 484 helpTipPlacement="right" 485 /> 486 <Switch 487 value="bucket_quota" 488 id="bucket_quota" 489 name="bucket_quota" 490 checked={quotaEnabled} 491 onChange={(event: React.ChangeEvent<HTMLInputElement>) => { 492 dispatch(setQuota(event.target.checked)); 493 }} 494 label={"Quota"} 495 disabled={!distributedSetup} 496 helpTip={ 497 <Fragment> 498 Setting a{" "} 499 <a 500 href="https://min.io/docs/minio/linux/reference/minio-mc/mc-quota-set.html" 501 target="blank" 502 > 503 quota 504 </a>{" "} 505 assigns a hard limit to a bucket beyond which MinIO does 506 not allow writes. 507 </Fragment> 508 } 509 helpTipPlacement="right" 510 /> 511 {quotaEnabled && distributedSetup && ( 512 <Fragment> 513 <InputBox 514 type="string" 515 id="quota_size" 516 name="quota_size" 517 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 518 dispatch(setQuotaSize(e.target.value)); 519 }} 520 label="Capacity" 521 value={quotaSize} 522 required 523 min="1" 524 overlayObject={ 525 <InputUnitMenu 526 id={"quota_unit"} 527 onUnitChange={(newValue) => { 528 dispatch(setQuotaUnit(newValue)); 529 }} 530 unitSelected={quotaUnit} 531 unitsList={k8sScalarUnitsExcluding(["Ki"])} 532 disabled={false} 533 /> 534 } 535 error={ 536 invalidFields.includes("quotaSize") 537 ? "Please enter a valid quota" 538 : "" 539 } 540 /> 541 </Fragment> 542 )} 543 {versioningEnabled && distributedSetup && lockingAllowed && ( 544 <Switch 545 value="bucket_retention" 546 id="bucket_retention" 547 name="bucket_retention" 548 checked={retentionEnabled} 549 onChange={(event: React.ChangeEvent<HTMLInputElement>) => { 550 dispatch(setRetention(event.target.checked)); 551 }} 552 label={"Retention"} 553 helpTip={ 554 <Fragment> 555 MinIO supports setting both{" "} 556 <a 557 href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#configure-bucket-default-object-retention" 558 target="blank" 559 > 560 bucket-default 561 </a>{" "} 562 and per-object retention rules. 563 <br /> 564 <br /> For per-object retention settings, defer to the 565 documentation for the PUT operation used by your 566 preferred SDK. 567 </Fragment> 568 } 569 helpTipPlacement="right" 570 /> 571 )} 572 {retentionEnabled && distributedSetup && ( 573 <Fragment> 574 <RadioGroup 575 currentValue={retentionMode} 576 id="retention_mode" 577 name="retention_mode" 578 label="Mode" 579 onChange={(e: React.ChangeEvent<{ value: unknown }>) => { 580 dispatch( 581 setRetentionMode( 582 e.target.value as ObjectRetentionMode, 583 ), 584 ); 585 }} 586 selectorOptions={[ 587 { value: "compliance", label: "Compliance" }, 588 { value: "governance", label: "Governance" }, 589 ]} 590 helpTip={ 591 <Fragment> 592 {" "} 593 <a 594 href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#minio-object-locking-compliance" 595 target="blank" 596 > 597 Compliance 598 </a>{" "} 599 lock protects Objects from write operations by all 600 users, including the MinIO root user. 601 <br /> 602 <br /> 603 <a 604 href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#minio-object-locking-governance" 605 target="blank" 606 > 607 Governance 608 </a>{" "} 609 lock protects Objects from write operations by 610 non-privileged users. 611 </Fragment> 612 } 613 helpTipPlacement="right" 614 /> 615 <InputBox 616 type="number" 617 id="retention_validity" 618 name="retention_validity" 619 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 620 dispatch(setRetentionValidity(e.target.valueAsNumber)); 621 }} 622 label="Validity" 623 value={String(retentionValidity)} 624 required 625 overlayObject={ 626 <InputUnitMenu 627 id={"retention_unit"} 628 onUnitChange={(newValue) => { 629 dispatch(setRetentionUnit(newValue)); 630 }} 631 unitSelected={retentionUnit} 632 unitsList={[ 633 { value: "days", label: "Days" }, 634 { value: "years", label: "Years" }, 635 ]} 636 disabled={false} 637 /> 638 } 639 /> 640 </Fragment> 641 )} 642 </Box> 643 </Box> 644 <Grid 645 item 646 xs={12} 647 sx={{ 648 display: "flex", 649 justifyContent: "flex-end", 650 alignItems: "center", 651 gap: 10, 652 marginTop: 15, 653 }} 654 > 655 <Button 656 id={"clear"} 657 type="button" 658 variant={"regular"} 659 className={"clearButton"} 660 onClick={resForm} 661 label={"Clear"} 662 /> 663 <TooltipWrapper 664 tooltip={ 665 invalidFields.length > 0 || !isDirty || hasErrors 666 ? "You must apply a valid name to the bucket" 667 : "" 668 } 669 > 670 <Button 671 id={"create-bucket"} 672 type="submit" 673 variant="callAction" 674 color="primary" 675 disabled={ 676 addLoading || 677 invalidFields.length > 0 || 678 !isDirty || 679 hasErrors 680 } 681 label={"Create Bucket"} 682 /> 683 </TooltipWrapper> 684 </Grid> 685 {addLoading && ( 686 <Grid item xs={12}> 687 <ProgressBar /> 688 </Grid> 689 )} 690 </form> 691 </FormLayout> 692 </PageLayout> 693 </Fragment> 694 ); 695 }; 696 697 export default AddBucket;