diff --git a/portal-ui/src/icons/DisabledIcon.tsx b/portal-ui/src/icons/DisabledIcon.tsx new file mode 100644 index 000000000..6c7cb65b8 --- /dev/null +++ b/portal-ui/src/icons/DisabledIcon.tsx @@ -0,0 +1,64 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import React, { SVGProps } from "react"; + +const DisabledIcon = (props: SVGProps) => { + return ( + + + + + + + + + + + + + + ); +}; + +export default DisabledIcon; diff --git a/portal-ui/src/icons/EnabledIcon.tsx b/portal-ui/src/icons/EnabledIcon.tsx new file mode 100644 index 000000000..6ea3d2d6c --- /dev/null +++ b/portal-ui/src/icons/EnabledIcon.tsx @@ -0,0 +1,64 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import React, { SVGProps } from "react"; + +const EnabledIcon = (props: SVGProps) => { + return ( + + + + + + + + + + + + + + ); +}; + +export default EnabledIcon; diff --git a/portal-ui/src/icons/HardBucketQuotaIcon.tsx b/portal-ui/src/icons/HardBucketQuotaIcon.tsx new file mode 100644 index 000000000..201f3c23c --- /dev/null +++ b/portal-ui/src/icons/HardBucketQuotaIcon.tsx @@ -0,0 +1,83 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import React, { SVGProps } from "react"; + +const HardBucketQuotaIcon = (props: SVGProps) => { + return ( + + + + + + + + + + + + ); +}; + +export default HardBucketQuotaIcon; diff --git a/portal-ui/src/icons/ReportedUsageFullIcon.tsx b/portal-ui/src/icons/ReportedUsageFullIcon.tsx new file mode 100644 index 000000000..6b692bb85 --- /dev/null +++ b/portal-ui/src/icons/ReportedUsageFullIcon.tsx @@ -0,0 +1,74 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import React, { SVGProps } from "react"; + +const ReportedUsageFullIcon = (props: SVGProps) => { + return ( + + + + + + + + + + + + + + + + ); +}; + +export default ReportedUsageFullIcon; diff --git a/portal-ui/src/icons/index.ts b/portal-ui/src/icons/index.ts index 2d957c252..6c8dbd5bb 100644 --- a/portal-ui/src/icons/index.ts +++ b/portal-ui/src/icons/index.ts @@ -142,3 +142,7 @@ export { default as BucketReplicationIcon } from "./BucketReplicationIcon"; export { default as EventSubscriptionIcon } from "./EventSubscriptionIcon"; export { default as ConfirmModalIcon } from "./ConfirmModalIcon"; export { default as ConfirmDeleteIcon } from "./ConfirmDeleteIcon"; +export { default as EnabledIcon } from "./EnabledIcon"; +export { default as DisabledIcon } from "./DisabledIcon"; +export { default as HardBucketQuotaIcon } from "./HardBucketQuotaIcon"; +export { default as ReportedUsageFullIcon } from "./ReportedUsageFullIcon"; diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessDetailsPanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessDetailsPanel.tsx index 53492352a..be5a058f6 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessDetailsPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessDetailsPanel.tsx @@ -38,6 +38,10 @@ import PanelTitle from "../../Common/PanelTitle/PanelTitle"; import SecureComponent, { hasPermission, } from "../../../../common/SecureComponent/SecureComponent"; +import { Theme } from "@mui/material/styles"; +import createStyles from "@mui/styles/createStyles"; +import { tableStyles } from "../../Common/FormComponents/common/styleLibrary"; +import withStyles from "@mui/styles/withStyles"; const mapState = (state: AppState) => ({ session: state.console.session, @@ -63,10 +67,15 @@ interface IAccessDetailsProps { bucketInfo: BucketInfo | null; } +const styles = (theme: Theme) => + createStyles({ + ...tableStyles, + }); const AccessDetails = ({ match, setErrorSnackMessage, loadingBucket, + classes, }: IAccessDetailsProps) => { const [curTab, setCurTab] = useState(0); const [loadingPolicies, setLoadingPolicies] = useState(true); @@ -181,7 +190,7 @@ const AccessDetails = ({ {displayPoliciesList && } {displayUsersList && } - + marginLeft: "10px", align: "right", }, + ...tableStyles, ...actionsTray, ...searchField, ...objectBrowserCommon, @@ -238,7 +240,7 @@ const AccessRule = ({ - + import("../../../../icons/BucketsIcon")); -const DeleteIcon = React.lazy(() => import("../../../../icons/DeleteIcon")); const FolderIcon = React.lazy(() => import("../../../../icons/FolderIcon")); const DeleteBucket = withSuspense( @@ -84,9 +80,12 @@ const BucketLifecyclePanel = withSuspense( const styles = (theme: Theme) => createStyles({ pageContainer: { - border: "1px solid #EAEAEA", height: "100%", }, + screenTitle: { + border: 0, + paddingTop: 0, + }, ...pageContentStyles, breadcrumLink: { textDecoration: "none", @@ -96,6 +95,11 @@ const styles = (theme: Theme) => capitalize: { textTransform: "capitalize", }, + deleteBtn: { + color: "#f44336", + border: "1px solid rgba(244, 67, 54, 0.5)", + maxWidth: 140, + }, ...hrClass, ...buttonsStyles, ...containerForHeader(theme.spacing(4)), @@ -105,10 +109,8 @@ interface IBucketDetailsProps { classes: any; match: any; history: any; - selectedTab: string; distributedSetup: boolean; setErrorSnackMessage: typeof setErrorSnackMessage; - setBucketDetailsTab: typeof setBucketDetailsTab; setBucketDetailsLoad: typeof setBucketDetailsLoad; loadingBucket: boolean; setBucketInfo: typeof setBucketInfo; @@ -119,9 +121,7 @@ const BucketDetails = ({ classes, match, history, - selectedTab, setErrorSnackMessage, - setBucketDetailsTab, distributedSetup, setBucketDetailsLoad, loadingBucket, @@ -240,6 +240,9 @@ const BucketDetails = ({ @@ -251,8 +254,11 @@ const BucketDetails = ({ scopes={[IAM_SCOPES.S3_GET_BUCKET_POLICY]} resource={bucketName} > - Access:{" "} - + Access: + {bucketInfo?.access.toLowerCase()} @@ -267,160 +273,157 @@ const BucketDetails = ({ resource={bucketName} errorProps={{ disabled: true }} > - { setDeleteOpen(true); }} - size="large" - > - - + text={`Delete Bucket`} + icon={} + /> - { setBucketDetailsLoad(true); }} - size="large" - variant={"contained"} - > - - + text={`Refresh`} + icon={} + color={"primary"} + /> } /> - - - - - - {distributedSetup && ( + + + + - )} - {distributedSetup && ( - )} - - - - ( - + {distributedSetup && ( + )} - /> - - - - } - > - {{ - tabConfig: { - label: "Summary", - value: "summary", - component: Link, - to: getRoutePath("summary"), - }, - }} - {{ - tabConfig: { - label: "Events", - value: "events", - component: Link, - disabled: !hasPermission(bucketName, [ - IAM_SCOPES.S3_GET_BUCKET_NOTIFICATIONS, - IAM_SCOPES.S3_PUT_BUCKET_NOTIFICATIONS, - ]), - to: getRoutePath("events"), - }, - }} - {{ - tabConfig: { - label: "Replication", - value: "replication", - component: Link, - disabled: - !distributedSetup || - !hasPermission(bucketName, [ - IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION, - IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION, + {distributedSetup && ( + + )} + + + + ( + + )} + /> + + + + } + > + {{ + tabConfig: { + label: "Summary", + value: "summary", + component: Link, + to: getRoutePath("summary"), + }, + }} + {{ + tabConfig: { + label: "Events", + value: "events", + component: Link, + disabled: !hasPermission(bucketName, [ + IAM_SCOPES.S3_GET_BUCKET_NOTIFICATIONS, + IAM_SCOPES.S3_PUT_BUCKET_NOTIFICATIONS, ]), - to: getRoutePath("replication"), - }, - }} - {{ - tabConfig: { - label: "Lifecycle", - value: "lifecycle", - component: Link, - disabled: - !distributedSetup || - !hasPermission(bucketName, [ - IAM_SCOPES.S3_GET_LIFECYCLE_CONFIGURATION, - IAM_SCOPES.S3_PUT_LIFECYCLE_CONFIGURATION, + to: getRoutePath("events"), + }, + }} + {{ + tabConfig: { + label: "Replication", + value: "replication", + component: Link, + disabled: + !distributedSetup || + !hasPermission(bucketName, [ + IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION, + IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION, + ]), + to: getRoutePath("replication"), + }, + }} + {{ + tabConfig: { + label: "Lifecycle", + value: "lifecycle", + component: Link, + disabled: + !distributedSetup || + !hasPermission(bucketName, [ + IAM_SCOPES.S3_GET_LIFECYCLE_CONFIGURATION, + IAM_SCOPES.S3_PUT_LIFECYCLE_CONFIGURATION, + ]), + to: getRoutePath("lifecycle"), + }, + }} + {{ + tabConfig: { + label: "Access Audit", + value: "access", + component: Link, + disabled: !hasPermission(bucketName, [ + IAM_SCOPES.ADMIN_GET_POLICY, + IAM_SCOPES.ADMIN_LIST_USER_POLICIES, + IAM_SCOPES.ADMIN_LIST_USERS, ]), - to: getRoutePath("lifecycle"), - }, - }} - {{ - tabConfig: { - label: "Access Audit", - value: "access", - component: Link, - disabled: !hasPermission(bucketName, [ - IAM_SCOPES.ADMIN_GET_POLICY, - IAM_SCOPES.ADMIN_LIST_USER_POLICIES, - IAM_SCOPES.ADMIN_LIST_USERS, - ]), - to: getRoutePath("access"), - }, - }} - {{ - tabConfig: { - label: "Access Rules", - value: "prefix", - component: Link, - disabled: !hasPermission(bucketName, [ - IAM_SCOPES.S3_GET_BUCKET_POLICY, - ]), - to: getRoutePath("prefix"), - }, - }} - + to: getRoutePath("access"), + }, + }} + {{ + tabConfig: { + label: "Access Rules", + value: "prefix", + component: Link, + disabled: !hasPermission(bucketName, [ + IAM_SCOPES.S3_GET_BUCKET_POLICY, + ]), + to: getRoutePath("prefix"), + }, + }} + + ); @@ -436,7 +439,6 @@ const mapState = (state: AppState) => ({ const connector = connect(mapState, { setErrorSnackMessage, - setBucketDetailsTab, setBucketDetailsLoad, setBucketInfo, }); diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketSummaryPanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketSummaryPanel.tsx index c8364d5c3..4ce982bc9 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketSummaryPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketSummaryPanel.tsx @@ -19,11 +19,9 @@ import { connect } from "react-redux"; import { Theme } from "@mui/material/styles"; import createStyles from "@mui/styles/createStyles"; import withStyles from "@mui/styles/withStyles"; -import { Button, CircularProgress } from "@mui/material"; +import { Box } from "@mui/material"; import get from "lodash/get"; -import Paper from "@mui/material/Paper"; import Grid from "@mui/material/Grid"; -import Typography from "@mui/material/Typography"; import { AppState } from "../../../../store"; import { setErrorSnackMessage } from "../../../../actions"; import { @@ -34,38 +32,30 @@ import { BucketReplication, BucketVersioning, } from "../types"; -import { niceBytes } from "../../../../common/utils"; -import { Bucket, BucketList } from "../../Watch/types"; +import { BucketList } from "../../Watch/types"; import { - buttonsStyles, - hrClass, + spacingUtils, + textStyleUtils, } from "../../Common/FormComponents/common/styleLibrary"; import { ErrorResponseHandler, IRetentionConfig, } from "../../../../common/types"; import api from "../../../../common/api"; -import GavelIcon from "@mui/icons-material/Gavel"; import { setBucketDetailsLoad } from "../actions"; -import ReportedUsageIcon from "../../../../icons/ReportedUsageIcon"; import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions"; -import PanelTitle from "../../Common/PanelTitle/PanelTitle"; -import Chip from "@mui/material/Chip"; -import AddIcon from "@mui/icons-material/Add"; -import CloseIcon from "@mui/icons-material/Close"; - import SecureComponent, { hasPermission, } from "../../../../common/SecureComponent/SecureComponent"; import withSuspense from "../../Common/Components/withSuspense"; +import LabelValuePair from "../../Common/UsageBarWrapper/LabelValuePair"; +import LabelWithIcon from "./SummaryItems/LabelWithIcon"; +import { EnabledIcon, DisabledIcon } from "../../../../icons"; +import EditablePropertyItem from "./SummaryItems/EditablePropertyItem"; +import ReportedUsage from "./SummaryItems/ReportedUsage"; +import BucketQuotaSize from "./SummaryItems/BucketQuotaSize"; -const AddBucketTagModal = withSuspense( - React.lazy(() => import("./AddBucketTagModal")) -); -const DeleteBucketTagModal = withSuspense( - React.lazy(() => import("./DeleteBucketTagModal")) -); const SetAccessPolicy = withSuspense( React.lazy(() => import("./SetAccessPolicy")) ); @@ -78,6 +68,10 @@ const EnableBucketEncryption = withSuspense( const EnableVersioningModal = withSuspense( React.lazy(() => import("./EnableVersioningModal")) ); +const BucketTags = withSuspense( + React.lazy(() => import("./SummaryItems/BucketTags")) +); + const EnableQuota = withSuspense(React.lazy(() => import("./EnableQuota"))); interface IBucketSummaryProps { @@ -92,43 +86,17 @@ interface IBucketSummaryProps { const styles = (theme: Theme) => createStyles({ - paperContainer: { - padding: 15, - paddingLeft: 50, - display: "flex", - }, - elementTitle: { - fontWeight: 500, - color: "#777777", - fontSize: 14, - marginTop: -9, - }, - consumptionValue: { - color: "#000000", - fontSize: "48px", - fontWeight: "bold", - }, - reportedUsage: { - padding: "15px", - }, - capitalizeFirst: { - textTransform: "capitalize", - "& .min-icon": { - width: 16, - height: 16, - }, - }, - titleCol: { - width: "25%", - }, - tag: { - textTransform: "none", - marginRight: "5px", - }, - ...hrClass, - ...buttonsStyles, + ...spacingUtils, + ...textStyleUtils, }); +const twoColCssGridLayoutConfig = { + display: "grid", + gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" }, + gridAutoFlow: { xs: "dense", sm: "row" }, + gap: 2, +}; + const BucketSummary = ({ classes, match, @@ -147,7 +115,6 @@ const BucketSummary = ({ const [replicationRules, setReplicationRules] = useState(false); const [loadingObjectLocking, setLoadingLocking] = useState(true); const [loadingSize, setLoadingSize] = useState(true); - const [loadingTags, setLoadingTags] = useState(true); const [bucketLoading, setBucketLoading] = useState(true); const [loadingEncryption, setLoadingEncryption] = useState(true); const [loadingVersioning, setLoadingVersioning] = useState(true); @@ -169,11 +136,6 @@ const BucketSummary = ({ useState(false); const [enableVersioningOpen, setEnableVersioningOpen] = useState(false); - const [tags, setTags] = useState(null); - const [tagModalOpen, setTagModalOpen] = useState(false); - const [tagKeys, setTagKeys] = useState([]); - const [selectedTag, setSelectedTag] = useState(["", ""]); - const [deleteTagModalOpen, setDeleteTagModalOpen] = useState(false); const bucketName = match.params["bucketName"]; @@ -332,32 +294,6 @@ const BucketSummary = ({ } }, [loadingSize, setErrorSnackMessage, bucketName]); - useEffect(() => { - if (loadingTags) { - api - .invoke("GET", `/api/v1/buckets/${bucketName}`) - .then((res: Bucket) => { - if (res != null && res?.details != null) { - setTags(res?.details?.tags); - setTagKeys(Object.keys(res?.details?.tags)); - } - setLoadingTags(false); - }) - .catch((err: ErrorResponseHandler) => { - setLoadingTags(false); - setErrorSnackMessage(err); - }); - } - }, [ - loadingTags, - setErrorSnackMessage, - bucketName, - setTags, - tags, - setTagKeys, - tagKeys, - ]); - useEffect(() => { if (loadingReplication && distributedSetup) { api @@ -395,7 +331,6 @@ const BucketSummary = ({ setBucketDetailsLoad(true); setBucketLoading(true); setLoadingSize(true); - setLoadingTags(true); setLoadingVersioning(true); setLoadingEncryption(true); setLoadingRetention(true); @@ -433,34 +368,6 @@ const BucketSummary = ({ loadAllBucketData(); } }; - - const closeAddTagModal = (refresh: boolean) => { - setTagModalOpen(false); - if (refresh) { - loadAllBucketData(); - } - }; - - const cap = (str: string) => { - if (!str) { - return null; - } - return str[0].toUpperCase() + str.slice(1); - }; - - const deleteTag = (tagKey: string, tagLabel: string) => { - setSelectedTag([tagKey, tagLabel]); - setDeleteTagModalOpen(true); - }; - - const closeDeleteTagModal = (refresh: boolean) => { - setDeleteTagModalOpen(false); - - if (refresh) { - loadAllBucketData(); - } - }; - // @ts-ignore return ( @@ -506,302 +413,175 @@ const BucketSummary = ({ versioningCurrentState={isVersioned} /> )} - {tagModalOpen && ( - - )} - {deleteTagModalOpen && ( - - )} + - - Summary + +

+ Summary +

- - - - - - - - - - - - {distributedSetup && ( - - - - - - - - - - - - - - - )} - - - - - - - - - - - - - -
Access Policy: - - - -
Replication: - - {replicationRules ? "Enabled" : "Disabled"} - -
Object Locking:{!hasObjectLocking ? "Disabled" : "Enabled"}
Encryption: - {loadingEncryption ? ( - - ) : ( - - - - )} -
Tags: - {tagKeys && - tagKeys.map((tagKey: any, index: any) => { - const tag = get(tags, `${tagKey}`, ""); - if (tag !== "") { - return ( - - } - onDelete={() => { - deleteTag(tagKey, tag); - }} - /> - - ); - } - return null; - })} - - } - clickable - size="small" - label="Add tag" - color="primary" - variant="outlined" - onClick={() => { - setTagModalOpen(true); - }} - /> - -
-
- - - - - - - - Reported Usage - - - - - {niceBytes(bucketSize)} - - -
-
-
-
+ + + + + { + setAccessPolicyScreenOpen(true); + }} + isLoading={bucketLoading} + /> + + + + { + setEnableEncryptionScreenOpen(true); + }} + isLoading={loadingEncryption} + /> + + + + : } + label={ + + } + /> + } + /> + + + + : } + label={ + + } + /> + } + /> + + + + } + /> + + + + + + + + + {distributedSetup && ( - - - - -

Versioning

-
- - - - - - - - - - - -
Versioning: - {loadingVersioning ? ( - - ) : ( - - - - )} - Quota: - {loadingQuota ? ( - - ) : ( - - - - )} -
-
- {quotaEnabled && quota && ( - - - - - - - - {cap(quota?.type)} Quota - - - - - {niceBytes(`${quota?.quota}`)} - - - )} -
-
-
-
-
+ + +

+ Versioning +

+
+
+ + + + + + + + + {quotaEnabled && quota ? : null} + +
)} @@ -810,72 +590,86 @@ const BucketSummary = ({ scopes={[IAM_SCOPES.S3_GET_OBJECT_RETENTION]} resource={bucketName} > - - - -

Retention

-
- - - - - - {retentionConfig === null ? ( - - ) : ( - - - - - )} - - - {retentionConfig === null ? ( - - ) : ( - - - - - )} - - -
Status: - {loadingRetention ? ( - - ) : ( - - - - )} -  Mode: - {retentionConfig && retentionConfig.mode} -
Valitidy: - {retentionConfig && retentionConfig.validity}{" "} - {retentionConfig && - (retentionConfig.validity === 1 - ? retentionConfig.unit.slice(0, -1) - : retentionConfig.unit)} -
-
+ + +

+ Retention +

-
+
+ + + + { + setRetentionConfigOpen(true); + }} + isLoading={loadingRetention} + /> + + + {retentionConfig && retentionConfig.mode + ? retentionConfig.mode + : "-"} + + } + /> + + {retentionConfig && retentionConfig.validity}{" "} + {retentionConfig && + (retentionConfig.validity === 1 + ? retentionConfig.unit.slice(0, -1) + : retentionConfig.unit)} + + } + /> + + + + {/*Spacer*/} + + )}
diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/ActionLink.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/ActionLink.tsx new file mode 100644 index 000000000..5179a1d11 --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/ActionLink.tsx @@ -0,0 +1,66 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import React from "react"; +import { Button, CircularProgress } from "@mui/material"; + +type ActionLinkProps = { + isLoading: boolean; + onClick: () => void; + classes?: any; + label: any; + [x: string]: any; +}; +const ActionLink = ({ + isLoading, + onClick, + label, + ...restProps +}: ActionLinkProps) => { + return ( + + ); +}; + +export default ActionLink; diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/BucketQuotaSize.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/BucketQuotaSize.tsx new file mode 100644 index 000000000..e48d7e13a --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/BucketQuotaSize.tsx @@ -0,0 +1,61 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import React from "react"; +import { Box } from "@mui/material"; +import { niceBytes } from "../../../../../common/utils"; +import { HardBucketQuotaIcon } from "../../../../../icons"; + +const BucketQuotaSize = ({ quota }: { quota: any }) => { + return ( + + + + + + + + + ); +}; + +export default BucketQuotaSize; diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/BucketTags.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/BucketTags.tsx new file mode 100644 index 000000000..4c9e6afb0 --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/BucketTags.tsx @@ -0,0 +1,185 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { useEffect, useState } from "react"; +import { Bucket } from "../../../Watch/types"; +import { ErrorResponseHandler } from "../../../../../common/types"; +import useApi from "../../../Common/Hooks/useApi"; +import { Box, CircularProgress } from "@mui/material"; +import { IAM_SCOPES } from "../../../../../common/SecureComponent/permissions"; +import SecureComponent from "../../../../../common/SecureComponent/SecureComponent"; +import get from "lodash/get"; +import Chip from "@mui/material/Chip"; +import CloseIcon from "@mui/icons-material/Close"; +import AddIcon from "@mui/icons-material/Add"; +import withSuspense from "../../../Common/Components/withSuspense"; + +const AddBucketTagModal = withSuspense( + React.lazy(() => import("../AddBucketTagModal")) +); +const DeleteBucketTagModal = withSuspense( + React.lazy(() => import("../DeleteBucketTagModal")) +); + +type BucketTagProps = { + setErrorSnackMessage: (err: ErrorResponseHandler) => void; + bucketName: string; +}; + +const BucketTags = ({ setErrorSnackMessage, bucketName }: BucketTagProps) => { + const [tags, setTags] = useState(null); + const [tagModalOpen, setTagModalOpen] = useState(false); + const [tagKeys, setTagKeys] = useState([]); + const [selectedTag, setSelectedTag] = useState(["", ""]); + const [deleteTagModalOpen, setDeleteTagModalOpen] = useState(false); + + const closeAddTagModal = (refresh: boolean) => { + setTagModalOpen(false); + if (refresh) { + fetchTags(); + } + }; + + const deleteTag = (tagKey: string, tagLabel: string) => { + setSelectedTag([tagKey, tagLabel]); + setDeleteTagModalOpen(true); + }; + + const closeDeleteTagModal = (refresh: boolean) => { + setDeleteTagModalOpen(false); + + if (refresh) { + fetchTags(); + } + }; + + const onTagLoaded = (res: Bucket) => { + if (res != null && res?.details != null) { + setTags(res?.details?.tags); + setTagKeys(Object.keys(res?.details?.tags)); + } + }; + + const onTagLoadFailed = (err: ErrorResponseHandler) => { + setErrorSnackMessage(err); + }; + + const [isLoading, invokeTagsApi] = useApi(onTagLoaded, onTagLoadFailed); + + const fetchTags = () => { + invokeTagsApi("GET", `/api/v1/buckets/${bucketName}`); + }; + + useEffect(() => { + fetchTags(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [bucketName]); + + return ( + + {isLoading ? ( + + ) : null} + + + + {tagKeys && + tagKeys.map((tagKey: any, index: any) => { + const tag = get(tags, `${tagKey}`, ""); + if (tag !== "") { + return ( + + } + onDelete={() => { + deleteTag(tagKey, tag); + }} + /> + + ); + } + return null; + })} + + + + } + clickable + size="small" + label="Add tag" + color="primary" + variant="outlined" + onClick={() => { + setTagModalOpen(true); + }} + /> + + + + + {/** Modals **/} + + {tagModalOpen && ( + + )} + {deleteTagModalOpen && ( + + )} + + ); +}; + +export default BucketTags; diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/EditActionButton.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/EditActionButton.tsx new file mode 100644 index 000000000..38d489ee0 --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/EditActionButton.tsx @@ -0,0 +1,56 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import React from "react"; +import { IconButton } from "@mui/material"; +import EditIcon from "../../../../../icons/EditIcon"; +import { Theme } from "@mui/material/styles"; +import createStyles from "@mui/styles/createStyles"; +import withStyles from "@mui/styles/withStyles"; + +type EditActionButtonProps = { + disabled?: boolean; + onClick: () => void | any; + [x: string]: any; +}; + +const styles = (theme: Theme) => + createStyles({ + root: { + "&:hover": { + backgroundColor: "#E2E2E2", + }, + }, + }); + +const EditActionButton = ({ + disabled, + onClick, + ...restProps +}: EditActionButtonProps) => { + return ( + + + + ); +}; + +export default withStyles(styles)(EditActionButton); diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/EditablePropertyItem.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/EditablePropertyItem.tsx new file mode 100644 index 000000000..44bec014e --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/EditablePropertyItem.tsx @@ -0,0 +1,95 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import React from "react"; +import LabelValuePair from "../../../Common/UsageBarWrapper/LabelValuePair"; +import SecureComponent from "../../../../../common/SecureComponent/SecureComponent"; +import ActionLink from "./ActionLink"; +import { Box } from "@mui/material"; +import EditActionButton from "./EditActionButton"; + +type PolicyItemProps = { + isLoading: boolean; + resourceName: string; + iamScopes: string[]; + property: any; + value: any; + onEdit: () => void; +}; + +const SecureAction = ({ + resourceName, + iamScopes, + children, +}: { + resourceName: string; + iamScopes: string[]; + children: any; +}) => { + return ( + + {children} + + ); +}; + +const EditablePropertyItem = ({ + isLoading = true, + resourceName = "", + iamScopes, + property = null, + value = null, + onEdit, +}: PolicyItemProps) => { + return ( + + + + + } + /> + + + + + ); +}; + +export default EditablePropertyItem; diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/LabelWithIcon.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/LabelWithIcon.tsx new file mode 100644 index 000000000..8284c81b3 --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/LabelWithIcon.tsx @@ -0,0 +1,48 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import React from "react"; +import { Box } from "@mui/material"; + +type LabelWithIconProps = { + icon: React.ReactNode | null; + label: React.ReactNode | null; +}; + +const LabelWithIcon = ({ icon = null, label = null }: LabelWithIconProps) => { + return ( + +
+ {icon} +
+
{label}
+
+ ); +}; + +export default LabelWithIcon; diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/RBIconButton.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/RBIconButton.tsx new file mode 100644 index 000000000..c69b7c12a --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/RBIconButton.tsx @@ -0,0 +1,81 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import React from "react"; +import BoxIconButton from "../../../Common/BoxIconButton/BoxIconButton"; +import { Theme } from "@mui/material/styles"; +import createStyles from "@mui/styles/createStyles"; +import withStyles from "@mui/styles/withStyles"; +import { IconButtonProps } from "@mui/material"; + +type DeleteButtonProps = { + onClick: (e: any) => void; + text?: string; + disabled?: boolean; + size?: string; + tooltip?: string; + classes?: any; + icon?: React.ReactNode; +}; + +const styles = (theme: Theme) => + createStyles({ + root: { + "& .min-icon": { + width: 12, + marginLeft: "5px", + "@media (max-width: 900px)": { + width: 16, + marginLeft: 0, + }, + }, + }, + }); + +const RBIconButton = ({ + onClick, + text = "", + disabled = false, + tooltip, + classes, + icon = null, + ...restProps +}: Partial & DeleteButtonProps) => { + return ( + + {text} {icon} + + ); +}; +export default withStyles(styles)(RBIconButton); diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/ReportedUsage.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/ReportedUsage.tsx new file mode 100644 index 000000000..fc1243ee3 --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/ReportedUsage.tsx @@ -0,0 +1,60 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import React from "react"; +import { Box } from "@mui/material"; +import { niceBytes } from "../../../../../common/utils"; +import { ReportedUsageFullIcon } from "../../../../../icons"; + +const ReportedUsage = ({ bucketSize }: { bucketSize: string }) => { + return ( + + + + + + + + + ); +}; + +export default ReportedUsage; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx index 57e1029db..0b45a867e 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx @@ -18,6 +18,7 @@ import React, { Fragment, useCallback, useEffect, + useMemo, useRef, useState, } from "react"; @@ -163,6 +164,25 @@ const styles = (theme: Theme) => ...containerForHeader(theme.spacing(4)), }); +const baseDnDStyle = { + borderWidth: 2, + borderRadius: 2, + borderColor: "#eeeeee", + outline: "none", +}; + +const activeDnDStyle = { + borderStyle: "dashed", + backgroundColor: "#fafafa", + borderColor: "#2196f3", +}; + +const acceptDnDStyle = { + borderStyle: "dashed", + backgroundColor: "#fafafa", + borderColor: "#00e676", +}; + interface IListObjectsProps { classes: any; match: any; @@ -355,7 +375,8 @@ const ListObjects = ({ api .invoke( "GET", - `/api/v1/buckets/${bucketName}/rewind/${rewindParsed}${pathPrefix ? `?prefix=${encodeFileName(pathPrefix)}` : `` + `/api/v1/buckets/${bucketName}/rewind/${rewindParsed}${ + pathPrefix ? `?prefix=${encodeFileName(pathPrefix)}` : `` }` ) .then((res: RewindObjectList) => { @@ -404,7 +425,8 @@ const ListObjects = ({ api .invoke( "GET", - `/api/v1/buckets/${bucketName}/objects${pathPrefix ? `?prefix=${encodeFileName(pathPrefix)}` : `` + `/api/v1/buckets/${bucketName}/objects${ + pathPrefix ? `?prefix=${encodeFileName(pathPrefix)}` : `` }` ) .then((res: BucketObjectsList) => { @@ -438,7 +460,8 @@ const ListObjects = ({ api .invoke( "GET", - `/api/v1/buckets/${bucketName}/rewind/${rewindParsed}${pathPrefix ? `?prefix=${encodeFileName(pathPrefix)}` : `` + `/api/v1/buckets/${bucketName}/rewind/${rewindParsed}${ + pathPrefix ? `?prefix=${encodeFileName(pathPrefix)}` : `` }` ) .then((res: RewindObjectList) => { @@ -463,7 +486,8 @@ const ListObjects = ({ api .invoke( "GET", - `/api/v1/buckets/${bucketName}/objects${internalPaths ? `?prefix=${internalPaths}` : `` + `/api/v1/buckets/${bucketName}/objects${ + internalPaths ? `?prefix=${internalPaths}` : `` }` ) .then((res: BucketObjectsList) => { @@ -647,8 +671,9 @@ const ListObjects = ({ }; const openPath = (idElement: string) => { - const newPath = `/buckets/${bucketName}/browse${idElement ? `/${encodeFileName(idElement)}` : `` - }`; + const newPath = `/buckets/${bucketName}/browse${ + idElement ? `/${encodeFileName(idElement)}` : `` + }`; history.push(newPath); return; }; @@ -691,7 +716,8 @@ const ListObjects = ({ .join("/"); encodedPath = encodeFileName( - `${path}${finalFolderPath}${!finalFolderPath.endsWith("/") ? "/" : "" + `${path}${finalFolderPath}${ + !finalFolderPath.endsWith("/") ? "/" : "" }` ); } @@ -718,10 +744,12 @@ const ListObjects = ({ xhr.open("POST", uploadUrl, true); const areMultipleFiles = files.length > 1; - const errorMessage = `An error occurred while uploading the file${areMultipleFiles ? "s" : "" - }.`; - const okMessage = `Object${areMultipleFiles ? "s" : `` - } uploaded successfully.`; + const errorMessage = `An error occurred while uploading the file${ + areMultipleFiles ? "s" : "" + }.`; + const okMessage = `Object${ + areMultipleFiles ? "s" : `` + } uploaded successfully.`; xhr.withCredentials = false; xhr.onload = function (event) { @@ -813,10 +841,20 @@ const ListObjects = ({ [uploadObject] ); - const { getRootProps, getInputProps } = useDropzone({ - noClick: true, - onDrop, - }); + const { getRootProps, getInputProps, isDragActive, isDragAccept } = + useDropzone({ + noClick: true, + onDrop, + }); + + const dndStyles = useMemo( + () => ({ + ...baseDnDStyle, + ...(isDragActive ? activeDnDStyle : {}), + ...(isDragAccept ? acceptDnDStyle : {}), + }), + [isDragActive, isDragAccept] + ); const openPreview = (fileObject: BucketObject) => { setSelectedPreview(fileObject); @@ -1288,7 +1326,7 @@ const ListObjects = ({
-
+
top: 3, }, }, + tabsContainer: { + border: "1px solid #eaeaea", + borderTop: 0, + }, ...hrClass, ...buttonsStyles, ...actionsTray, @@ -607,7 +611,11 @@ const ObjectDetails = ({ } /> - + {{ tabConfig: { label: "Details", diff --git a/portal-ui/src/screens/Console/Common/FormComponents/common/styleLibrary.ts b/portal-ui/src/screens/Console/Common/FormComponents/common/styleLibrary.ts index 4ea4ce5e0..a7a2736b8 100644 --- a/portal-ui/src/screens/Console/Common/FormComponents/common/styleLibrary.ts +++ b/portal-ui/src/screens/Console/Common/FormComponents/common/styleLibrary.ts @@ -1259,3 +1259,9 @@ export const modalStyleUtils: any = { paddingTop: 10, }, }; + +export const textStyleUtils: any = { + textMuted: { + color: "#8399AB", + }, +}; diff --git a/portal-ui/src/screens/Console/Common/ScreenTitle/ScreenTitle.tsx b/portal-ui/src/screens/Console/Common/ScreenTitle/ScreenTitle.tsx index 159bea362..82cc74367 100644 --- a/portal-ui/src/screens/Console/Common/ScreenTitle/ScreenTitle.tsx +++ b/portal-ui/src/screens/Console/Common/ScreenTitle/ScreenTitle.tsx @@ -51,6 +51,7 @@ const styles = (theme: Theme) => alignItems: "center", justifyContent: "space-between", padding: "1rem", + borderBottom: "1px solid #EAEAEA", "@media (max-width: 600px)": { flexFlow: "column", @@ -63,7 +64,7 @@ const styles = (theme: Theme) => flexFlow: "column", alignItems: "flex-start", "& h1": { - fontSize: "1.4rem", + fontSize: 19, }, }, leftItems: {