github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/BucketDetails/BucketSummaryPanel.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 { useSelector } from "react-redux"; 21 import { useParams } from "react-router-dom"; 22 import { api } from "api"; 23 import { 24 BucketEncryptionInfo, 25 BucketQuota, 26 BucketVersioningResponse, 27 GetBucketRetentionConfig, 28 } from "api/consoleApi"; 29 import { errorToHandler } from "api/errors"; 30 import { 31 Box, 32 DisabledIcon, 33 EnabledIcon, 34 Grid, 35 SectionTitle, 36 ValuePair, 37 } from "mds"; 38 import { twoColCssGridLayoutConfig } from "../../Common/FormComponents/common/styleLibrary"; 39 import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions"; 40 import { 41 hasPermission, 42 SecureComponent, 43 } from "../../../../common/SecureComponent"; 44 import { 45 selDistSet, 46 setErrorSnackMessage, 47 setHelpName, 48 } from "../../../../systemSlice"; 49 import { 50 selBucketDetailsInfo, 51 selBucketDetailsLoading, 52 setBucketDetailsLoad, 53 } from "./bucketDetailsSlice"; 54 import { useAppDispatch } from "../../../../store"; 55 import VersioningInfo from "../VersioningInfo"; 56 import withSuspense from "../../Common/Components/withSuspense"; 57 import LabelWithIcon from "./SummaryItems/LabelWithIcon"; 58 import EditablePropertyItem from "./SummaryItems/EditablePropertyItem"; 59 import ReportedUsage from "./SummaryItems/ReportedUsage"; 60 import BucketQuotaSize from "./SummaryItems/BucketQuotaSize"; 61 62 const SetAccessPolicy = withSuspense( 63 React.lazy(() => import("./SetAccessPolicy")), 64 ); 65 const SetRetentionConfig = withSuspense( 66 React.lazy(() => import("./SetRetentionConfig")), 67 ); 68 const EnableBucketEncryption = withSuspense( 69 React.lazy(() => import("./EnableBucketEncryption")), 70 ); 71 const EnableVersioningModal = withSuspense( 72 React.lazy(() => import("./EnableVersioningModal")), 73 ); 74 const BucketTags = withSuspense( 75 React.lazy(() => import("./SummaryItems/BucketTags")), 76 ); 77 const EnableQuota = withSuspense(React.lazy(() => import("./EnableQuota"))); 78 79 const BucketSummary = () => { 80 const dispatch = useAppDispatch(); 81 const params = useParams(); 82 83 const loadingBucket = useSelector(selBucketDetailsLoading); 84 const bucketInfo = useSelector(selBucketDetailsInfo); 85 const distributedSetup = useSelector(selDistSet); 86 87 const [encryptionCfg, setEncryptionCfg] = 88 useState<BucketEncryptionInfo | null>(null); 89 const [bucketSize, setBucketSize] = useState<number | "0">("0"); 90 const [hasObjectLocking, setHasObjectLocking] = useState<boolean | undefined>( 91 false, 92 ); 93 const [accessPolicyScreenOpen, setAccessPolicyScreenOpen] = 94 useState<boolean>(false); 95 const [replicationRules, setReplicationRules] = useState<boolean>(false); 96 const [loadingObjectLocking, setLoadingLocking] = useState<boolean>(true); 97 const [loadingSize, setLoadingSize] = useState<boolean>(true); 98 const [bucketLoading, setBucketLoading] = useState<boolean>(true); 99 const [loadingEncryption, setLoadingEncryption] = useState<boolean>(true); 100 const [loadingVersioning, setLoadingVersioning] = useState<boolean>(true); 101 const [loadingQuota, setLoadingQuota] = useState<boolean>(true); 102 const [loadingReplication, setLoadingReplication] = useState<boolean>(true); 103 const [loadingRetention, setLoadingRetention] = useState<boolean>(true); 104 const [versioningInfo, setVersioningInfo] = 105 useState<BucketVersioningResponse>(); 106 const [quotaEnabled, setQuotaEnabled] = useState<boolean>(false); 107 const [quota, setQuota] = useState<BucketQuota | null>(null); 108 const [encryptionEnabled, setEncryptionEnabled] = useState<boolean>(false); 109 const [retentionEnabled, setRetentionEnabled] = useState<boolean>(false); 110 const [retentionConfig, setRetentionConfig] = 111 useState<GetBucketRetentionConfig | null>(null); 112 const [retentionConfigOpen, setRetentionConfigOpen] = 113 useState<boolean>(false); 114 const [enableEncryptionScreenOpen, setEnableEncryptionScreenOpen] = 115 useState<boolean>(false); 116 const [enableQuotaScreenOpen, setEnableQuotaScreenOpen] = 117 useState<boolean>(false); 118 const [enableVersioningOpen, setEnableVersioningOpen] = 119 useState<boolean>(false); 120 useEffect(() => { 121 dispatch(setHelpName("bucket_detail_summary")); 122 // eslint-disable-next-line react-hooks/exhaustive-deps 123 }, []); 124 125 const bucketName = params.bucketName || ""; 126 127 let accessPolicy = "PRIVATE"; 128 let policyDefinition = ""; 129 130 if (bucketInfo !== null && bucketInfo.access && bucketInfo.definition) { 131 accessPolicy = bucketInfo.access; 132 policyDefinition = bucketInfo.definition; 133 } 134 135 const displayGetBucketObjectLockConfiguration = hasPermission(bucketName, [ 136 IAM_SCOPES.S3_GET_BUCKET_OBJECT_LOCK_CONFIGURATION, 137 IAM_SCOPES.S3_GET_ACTIONS, 138 ]); 139 140 const displayGetBucketEncryptionConfiguration = hasPermission(bucketName, [ 141 IAM_SCOPES.S3_GET_BUCKET_ENCRYPTION_CONFIGURATION, 142 IAM_SCOPES.S3_GET_ACTIONS, 143 ]); 144 145 const displayGetBucketQuota = hasPermission(bucketName, [ 146 IAM_SCOPES.ADMIN_GET_BUCKET_QUOTA, 147 ]); 148 149 useEffect(() => { 150 if (loadingBucket) { 151 setBucketLoading(true); 152 } else { 153 setBucketLoading(false); 154 } 155 }, [loadingBucket, setBucketLoading]); 156 157 useEffect(() => { 158 if (loadingEncryption) { 159 if (displayGetBucketEncryptionConfiguration) { 160 api.buckets 161 .getBucketEncryptionInfo(bucketName) 162 .then((res) => { 163 if (res.data.algorithm) { 164 setEncryptionEnabled(true); 165 setEncryptionCfg(res.data); 166 } 167 setLoadingEncryption(false); 168 }) 169 .catch((err) => { 170 err = errorToHandler(err.error); 171 if ( 172 err.errorMessage === 173 "The server side encryption configuration was not found" 174 ) { 175 setEncryptionEnabled(false); 176 setEncryptionCfg(null); 177 } 178 setLoadingEncryption(false); 179 }); 180 } else { 181 setEncryptionEnabled(false); 182 setEncryptionCfg(null); 183 setLoadingEncryption(false); 184 } 185 } 186 }, [loadingEncryption, bucketName, displayGetBucketEncryptionConfiguration]); 187 188 useEffect(() => { 189 if (loadingVersioning && distributedSetup) { 190 api.buckets 191 .getBucketVersioning(bucketName) 192 .then((res) => { 193 setVersioningInfo(res.data); 194 setLoadingVersioning(false); 195 }) 196 .catch((err) => { 197 dispatch(setErrorSnackMessage(errorToHandler(err.error))); 198 setLoadingVersioning(false); 199 }); 200 } 201 }, [loadingVersioning, dispatch, bucketName, distributedSetup]); 202 203 useEffect(() => { 204 if (loadingQuota && distributedSetup) { 205 if (displayGetBucketQuota) { 206 api.buckets 207 .getBucketQuota(bucketName) 208 .then((res) => { 209 setQuota(res.data); 210 if (res.data.quota) { 211 setQuotaEnabled(true); 212 } else { 213 setQuotaEnabled(false); 214 } 215 setLoadingQuota(false); 216 }) 217 .catch((err) => { 218 dispatch(setErrorSnackMessage(errorToHandler(err.error))); 219 setQuotaEnabled(false); 220 setLoadingQuota(false); 221 }); 222 } else { 223 setQuotaEnabled(false); 224 setLoadingQuota(false); 225 } 226 } 227 }, [ 228 loadingQuota, 229 setLoadingVersioning, 230 dispatch, 231 bucketName, 232 distributedSetup, 233 displayGetBucketQuota, 234 ]); 235 236 useEffect(() => { 237 if (loadingVersioning && distributedSetup) { 238 if (displayGetBucketObjectLockConfiguration) { 239 api.buckets 240 .getBucketObjectLockingStatus(bucketName) 241 .then((res) => { 242 setHasObjectLocking(res.data.object_locking_enabled); 243 setLoadingLocking(false); 244 }) 245 .catch((err) => { 246 dispatch(setErrorSnackMessage(errorToHandler(err.error))); 247 setLoadingLocking(false); 248 }); 249 } else { 250 setLoadingLocking(false); 251 } 252 } 253 }, [ 254 loadingObjectLocking, 255 dispatch, 256 bucketName, 257 loadingVersioning, 258 distributedSetup, 259 displayGetBucketObjectLockConfiguration, 260 ]); 261 262 useEffect(() => { 263 if (loadingSize) { 264 api.buckets 265 .listBuckets() 266 .then((res) => { 267 const resBuckets = get(res.data, "buckets", []); 268 269 const bucketInfo = resBuckets.find( 270 (bucket) => bucket.name === bucketName, 271 ); 272 273 const size = get(bucketInfo, "size", "0"); 274 275 setLoadingSize(false); 276 setBucketSize(size); 277 }) 278 .catch((err) => { 279 setLoadingSize(false); 280 dispatch(setErrorSnackMessage(errorToHandler(err.error))); 281 }); 282 } 283 }, [loadingSize, dispatch, bucketName]); 284 285 useEffect(() => { 286 if (loadingReplication && distributedSetup) { 287 api.buckets 288 .getBucketReplication(bucketName) 289 .then((res) => { 290 const r = res.data.rules ? res.data.rules : []; 291 setReplicationRules(r.length > 0); 292 setLoadingReplication(false); 293 }) 294 .catch((err) => { 295 dispatch(setErrorSnackMessage(errorToHandler(err.error))); 296 setLoadingReplication(false); 297 }); 298 } 299 }, [loadingReplication, dispatch, bucketName, distributedSetup]); 300 301 useEffect(() => { 302 if (loadingRetention && hasObjectLocking) { 303 api.buckets 304 .getBucketRetentionConfig(bucketName) 305 .then((res) => { 306 setLoadingRetention(false); 307 setRetentionEnabled(true); 308 setRetentionConfig(res.data); 309 }) 310 .catch((err) => { 311 setRetentionEnabled(false); 312 setLoadingRetention(false); 313 setRetentionConfig(null); 314 }); 315 } 316 }, [loadingRetention, hasObjectLocking, bucketName]); 317 318 const loadAllBucketData = () => { 319 dispatch(setBucketDetailsLoad(true)); 320 setBucketLoading(true); 321 setLoadingSize(true); 322 setLoadingVersioning(true); 323 setLoadingEncryption(true); 324 setLoadingRetention(true); 325 }; 326 327 const setBucketVersioning = () => { 328 setEnableVersioningOpen(true); 329 }; 330 const setBucketQuota = () => { 331 setEnableQuotaScreenOpen(true); 332 }; 333 334 const closeEnableBucketEncryption = () => { 335 setEnableEncryptionScreenOpen(false); 336 setLoadingEncryption(true); 337 }; 338 const closeEnableBucketQuota = () => { 339 setEnableQuotaScreenOpen(false); 340 setLoadingQuota(true); 341 }; 342 343 const closeSetAccessPolicy = () => { 344 setAccessPolicyScreenOpen(false); 345 loadAllBucketData(); 346 }; 347 348 const closeRetentionConfig = () => { 349 setRetentionConfigOpen(false); 350 loadAllBucketData(); 351 }; 352 353 const closeEnableVersioning = (refresh: boolean) => { 354 setEnableVersioningOpen(false); 355 if (refresh) { 356 loadAllBucketData(); 357 } 358 }; 359 360 let versioningStatus = versioningInfo?.status; 361 let versioningText = "Unversioned (Default)"; 362 if (versioningStatus === "Enabled") { 363 versioningText = "Versioned"; 364 } else if (versioningStatus === "Suspended") { 365 versioningText = "Suspended"; 366 } 367 368 return ( 369 <Fragment> 370 {enableEncryptionScreenOpen && ( 371 <EnableBucketEncryption 372 open={enableEncryptionScreenOpen} 373 selectedBucket={bucketName} 374 encryptionEnabled={encryptionEnabled} 375 encryptionCfg={encryptionCfg} 376 closeModalAndRefresh={closeEnableBucketEncryption} 377 /> 378 )} 379 {enableQuotaScreenOpen && ( 380 <EnableQuota 381 open={enableQuotaScreenOpen} 382 selectedBucket={bucketName} 383 enabled={quotaEnabled} 384 cfg={quota} 385 closeModalAndRefresh={closeEnableBucketQuota} 386 /> 387 )} 388 {accessPolicyScreenOpen && ( 389 <SetAccessPolicy 390 bucketName={bucketName} 391 open={accessPolicyScreenOpen} 392 actualPolicy={accessPolicy} 393 actualDefinition={policyDefinition} 394 closeModalAndRefresh={closeSetAccessPolicy} 395 /> 396 )} 397 {retentionConfigOpen && ( 398 <SetRetentionConfig 399 bucketName={bucketName} 400 open={retentionConfigOpen} 401 closeModalAndRefresh={closeRetentionConfig} 402 /> 403 )} 404 {enableVersioningOpen && ( 405 <EnableVersioningModal 406 closeVersioningModalAndRefresh={closeEnableVersioning} 407 modalOpen={enableVersioningOpen} 408 selectedBucket={bucketName} 409 versioningInfo={versioningInfo} 410 objectLockingEnabled={!!hasObjectLocking} 411 /> 412 )} 413 414 <SectionTitle separator sx={{ marginBottom: 15 }}> 415 Summary 416 </SectionTitle> 417 <Grid container> 418 <SecureComponent 419 scopes={[IAM_SCOPES.S3_GET_BUCKET_POLICY, IAM_SCOPES.S3_GET_ACTIONS]} 420 resource={bucketName} 421 > 422 <Grid item xs={12}> 423 <Box sx={twoColCssGridLayoutConfig}> 424 <Box sx={twoColCssGridLayoutConfig}> 425 <SecureComponent 426 scopes={[ 427 IAM_SCOPES.S3_GET_BUCKET_POLICY, 428 IAM_SCOPES.S3_GET_ACTIONS, 429 ]} 430 resource={bucketName} 431 > 432 <EditablePropertyItem 433 iamScopes={[ 434 IAM_SCOPES.S3_PUT_BUCKET_POLICY, 435 IAM_SCOPES.S3_PUT_ACTIONS, 436 ]} 437 resourceName={bucketName} 438 property={"Access Policy:"} 439 value={accessPolicy.toLowerCase()} 440 onEdit={() => { 441 setAccessPolicyScreenOpen(true); 442 }} 443 isLoading={bucketLoading} 444 helpTip={ 445 <Fragment> 446 <strong>Private</strong> policy limits access to 447 credentialled accounts with appropriate permissions 448 <br /> 449 <strong>Public</strong> policy anyone will be able to 450 upload, download and delete files from this Bucket once 451 logged in 452 <br /> 453 <strong>Custom</strong> policy can be written to define 454 which accounts are authorized to access this Bucket 455 <br /> 456 <br /> 457 To allow Bucket access without credentials, use the{" "} 458 <a href={`/buckets/${bucketName}/admin/prefix`}> 459 Anonymous 460 </a>{" "} 461 setting 462 </Fragment> 463 } 464 /> 465 </SecureComponent> 466 467 <SecureComponent 468 scopes={[ 469 IAM_SCOPES.S3_GET_BUCKET_ENCRYPTION_CONFIGURATION, 470 IAM_SCOPES.S3_GET_ACTIONS, 471 ]} 472 resource={bucketName} 473 > 474 <EditablePropertyItem 475 iamScopes={[ 476 IAM_SCOPES.S3_PUT_BUCKET_ENCRYPTION_CONFIGURATION, 477 IAM_SCOPES.S3_PUT_ACTIONS, 478 ]} 479 resourceName={bucketName} 480 property={"Encryption:"} 481 value={encryptionEnabled ? "Enabled" : "Disabled"} 482 onEdit={() => { 483 setEnableEncryptionScreenOpen(true); 484 }} 485 isLoading={loadingEncryption} 486 helpTip={ 487 <Fragment> 488 MinIO supports enabling automatic{" "} 489 <a 490 href="https://min.io/docs/minio/kubernetes/upstream/administration/server-side-encryption/server-side-encryption-sse-kms.html" 491 target="blank" 492 > 493 SSE-KMS 494 </a>{" "} 495 and{" "} 496 <a 497 href="https://min.io/docs/minio/kubernetes/upstream/administration/server-side-encryption/server-side-encryption-sse-s3.html" 498 target="blank" 499 > 500 SSE-S3 501 </a>{" "} 502 encryption of all objects written to a bucket using a 503 specific External Key (EK) stored on the external KMS. 504 </Fragment> 505 } 506 /> 507 </SecureComponent> 508 509 <SecureComponent 510 scopes={[ 511 IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION, 512 IAM_SCOPES.S3_GET_ACTIONS, 513 ]} 514 resource={bucketName} 515 > 516 <ValuePair 517 label={"Replication:"} 518 value={ 519 <LabelWithIcon 520 icon={ 521 replicationRules ? <EnabledIcon /> : <DisabledIcon /> 522 } 523 label={ 524 <label className={"muted"}> 525 {replicationRules ? "Enabled" : "Disabled"} 526 </label> 527 } 528 /> 529 } 530 /> 531 </SecureComponent> 532 533 <SecureComponent 534 scopes={[ 535 IAM_SCOPES.S3_GET_BUCKET_OBJECT_LOCK_CONFIGURATION, 536 IAM_SCOPES.S3_GET_ACTIONS, 537 ]} 538 resource={bucketName} 539 > 540 <ValuePair 541 label={"Object Locking:"} 542 value={ 543 <LabelWithIcon 544 icon={ 545 hasObjectLocking ? <EnabledIcon /> : <DisabledIcon /> 546 } 547 label={ 548 <label className={"muted"}> 549 {hasObjectLocking ? "Enabled" : "Disabled"} 550 </label> 551 } 552 /> 553 } 554 /> 555 </SecureComponent> 556 <Box> 557 <ValuePair 558 label={"Tags:"} 559 value={<BucketTags bucketName={bucketName} />} 560 /> 561 </Box> 562 <EditablePropertyItem 563 iamScopes={[IAM_SCOPES.ADMIN_SET_BUCKET_QUOTA]} 564 resourceName={bucketName} 565 property={"Quota:"} 566 value={quotaEnabled ? "Enabled" : "Disabled"} 567 onEdit={setBucketQuota} 568 isLoading={loadingQuota} 569 helpTip={ 570 <Fragment> 571 Setting a{" "} 572 <a 573 href="https://min.io/docs/minio/linux/reference/minio-mc/mc-quota-set.html" 574 target="blank" 575 > 576 quota 577 </a>{" "} 578 assigns a hard limit to a bucket beyond which MinIO does 579 not allow writes. 580 </Fragment> 581 } 582 /> 583 </Box> 584 <Box 585 sx={{ 586 display: "grid", 587 gridTemplateColumns: "1fr", 588 alignItems: "flex-start", 589 }} 590 > 591 <ReportedUsage bucketSize={`${bucketSize}`} /> 592 {quotaEnabled && quota ? ( 593 <BucketQuotaSize quota={quota} /> 594 ) : null} 595 </Box> 596 </Box> 597 </Grid> 598 </SecureComponent> 599 600 {distributedSetup && ( 601 <SecureComponent 602 scopes={[ 603 IAM_SCOPES.S3_GET_BUCKET_VERSIONING, 604 IAM_SCOPES.S3_GET_ACTIONS, 605 ]} 606 resource={bucketName} 607 > 608 <Grid item xs={12} sx={{ marginTop: 5 }}> 609 <SectionTitle separator sx={{ marginBottom: 15 }}> 610 Versioning 611 </SectionTitle> 612 613 <Box sx={twoColCssGridLayoutConfig}> 614 <Box sx={twoColCssGridLayoutConfig}> 615 <EditablePropertyItem 616 iamScopes={[ 617 IAM_SCOPES.S3_PUT_BUCKET_VERSIONING, 618 IAM_SCOPES.S3_PUT_ACTIONS, 619 ]} 620 resourceName={bucketName} 621 property={"Current Status:"} 622 value={ 623 <Box 624 sx={{ 625 display: "flex", 626 flexDirection: "column", 627 textDecorationStyle: "initial", 628 placeItems: "flex-start", 629 justifyItems: "flex-start", 630 gap: 3, 631 }} 632 > 633 <div> {versioningText}</div> 634 </Box> 635 } 636 onEdit={setBucketVersioning} 637 isLoading={loadingVersioning} 638 disabled={hasObjectLocking} 639 /> 640 641 {versioningInfo?.status === "Enabled" ? ( 642 <VersioningInfo versioningState={versioningInfo} /> 643 ) : null} 644 </Box> 645 </Box> 646 </Grid> 647 </SecureComponent> 648 )} 649 650 {hasObjectLocking && ( 651 <SecureComponent 652 scopes={[ 653 IAM_SCOPES.S3_GET_OBJECT_RETENTION, 654 IAM_SCOPES.S3_GET_ACTIONS, 655 ]} 656 resource={bucketName} 657 > 658 <Grid item xs={12} sx={{ marginTop: 5 }}> 659 <SectionTitle separator sx={{ marginBottom: 15 }}> 660 Retention 661 </SectionTitle> 662 663 <Box sx={twoColCssGridLayoutConfig}> 664 <Box sx={twoColCssGridLayoutConfig}> 665 <EditablePropertyItem 666 iamScopes={[IAM_SCOPES.ADMIN_SET_BUCKET_QUOTA]} 667 resourceName={bucketName} 668 property={"Retention:"} 669 value={retentionEnabled ? "Enabled" : "Disabled"} 670 onEdit={() => { 671 setRetentionConfigOpen(true); 672 }} 673 isLoading={loadingRetention} 674 helpTip={ 675 <Fragment> 676 MinIO{" "} 677 <a 678 target="blank" 679 href="https://min.io/docs/minio/macos/administration/object-management.html#object-retention" 680 > 681 Object Locking 682 </a>{" "} 683 enforces Write-Once Read-Many (WORM) immutability to 684 protect versioned objects from deletion. 685 </Fragment> 686 } 687 /> 688 689 <ValuePair 690 label={"Mode:"} 691 value={ 692 <label 693 className={"muted"} 694 style={{ textTransform: "capitalize" }} 695 > 696 {retentionConfig && retentionConfig.mode 697 ? retentionConfig.mode 698 : "-"} 699 </label> 700 } 701 /> 702 <ValuePair 703 label={"Validity:"} 704 value={ 705 <label 706 className={"muted"} 707 style={{ textTransform: "capitalize" }} 708 > 709 {retentionConfig && retentionConfig.validity}{" "} 710 {retentionConfig && 711 (retentionConfig.validity === 1 712 ? retentionConfig.unit?.slice(0, -1) 713 : retentionConfig.unit)} 714 </label> 715 } 716 /> 717 </Box> 718 </Box> 719 </Grid> 720 </SecureComponent> 721 )} 722 </Grid> 723 </Fragment> 724 ); 725 }; 726 727 export default BucketSummary;