UX Bucket summary (#1355)

This commit is contained in:
Prakash Senthil Vel
2022-01-04 05:30:38 +00:00
committed by GitHub
parent eae9f46ac4
commit b9ddadf9ce
21 changed files with 1445 additions and 643 deletions

View File

@@ -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 <http://www.gnu.org/licenses/>.
import React, { SVGProps } from "react";
const DisabledIcon = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 16 16"
{...props}
>
<defs>
<clipPath id="disabled-clip-path">
<rect
id="Rectángulo_1068"
data-name="Rectángulo 1068"
width="16"
height="16"
fill="none"
/>
</clipPath>
</defs>
<rect
id="Rectángulo_1065"
data-name="Rectángulo 1065"
width="16"
height="16"
fill="none"
/>
<g id="Grupo_2455" data-name="Grupo 2455">
<g
id="Grupo_2454"
data-name="Grupo 2454"
clipPath="url(#disabled-clip-path)"
>
<path
id="Trazado_7232"
data-name="Trazado 7232"
d="M8,0a8,8,0,1,0,8,8A8,8,0,0,0,8,0m3.235,5.4L8.965,8.174,10.949,10.6a.857.857,0,0,1-1.327,1.086h0L7.857,9.528,6.092,11.686A.857.857,0,0,1,4.765,10.6L6.749,8.174,4.479,5.4A.857.857,0,0,1,5.806,4.314L7.857,6.821l2.05-2.506A.857.857,0,1,1,11.235,5.4"
fill="#969fa8"
/>
</g>
</g>
</svg>
);
};
export default DisabledIcon;

View File

@@ -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 <http://www.gnu.org/licenses/>.
import React, { SVGProps } from "react";
const EnabledIcon = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 16 16"
{...props}
>
<defs>
<clipPath id="enabled-clip-path">
<rect
id="Rectángulo_1067"
data-name="Rectángulo 1067"
width="16"
height="16"
fill="none"
/>
</clipPath>
</defs>
<rect
id="Rectángulo_1066"
data-name="Rectángulo 1066"
width="16"
height="16"
fill="none"
/>
<g id="Grupo_2453" data-name="Grupo 2453">
<g
id="Grupo_2452"
data-name="Grupo 2452"
clipPath="url(#enabled-clip-path)"
>
<path
id="Trazado_7231"
data-name="Trazado 7231"
d="M8,0a8,8,0,1,0,8,8A8,8,0,0,0,8,0m4.575,5.769-.005.005L7.837,11.69a.89.89,0,0,1-.635.284H7.185a.889.889,0,0,1-.628-.26h0L3.421,8.577a.889.889,0,1,1,1.2-1.31q.028.025.053.053L7.16,9.8l4.117-5.246.024-.026h0a.889.889,0,0,1,1.275,1.24"
fill="#969fa8"
/>
</g>
</g>
</svg>
);
};
export default EnabledIcon;

View File

@@ -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 <http://www.gnu.org/licenses/>.
import React, { SVGProps } from "react";
const HardBucketQuotaIcon = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 36.369 36.346"
{...props}
>
<g id="hardquota-icn" transform="translate(-98.002 -28.027)">
<path
id="Trazado_7233"
data-name="Trazado 7233"
d="M344.76,203.93l2.664-2.664,8.15,8.15-2.664,2.664Z"
transform="translate(-228.962 -160.744)"
fill="#07193e"
/>
<path
id="Trazado_7234"
data-name="Trazado 7234"
d="M464.768,316.895a1.11,1.11,0,0,0-1.575,0l-2.827,2.827h0a1.111,1.111,0,0,0,0,1.575l5.182,5.182a1.114,1.114,0,0,0,.787.327,1.1,1.1,0,0,0,.808-.327l2.827-2.827a1.11,1.11,0,0,0,0-1.575Z"
transform="translate(-335.926 -267.73)"
fill="#07193e"
/>
<path
id="Trazado_7235"
data-name="Trazado 7235"
d="M235.486,84.317l-5.408-5.408a2.141,2.141,0,0,1-.157-.174L222.2,86.45c.061.052.121.105.178.161l5.4,5.4c.057.057.109.117.161.178l7.718-7.718a2.2,2.2,0,0,1-.178-.157Z"
transform="translate(-115.243 -47.051)"
fill="#07193e"
/>
<path
id="Trazado_7236"
data-name="Trazado 7236"
d="M337.566,36.693a1.912,1.912,0,0,0,2.706-2.7l-5.408-5.4a1.91,1.91,0,1,0-2.7,2.7Z"
transform="translate(-216.754)"
fill="#07193e"
/>
<path
id="Trazado_7237"
data-name="Trazado 7237"
d="M174.741,188.807a1.912,1.912,0,1,0-2.7,2.706l5.408,5.392a1.911,1.911,0,1,0,2.7-2.7Z"
transform="translate(-68.177 -148.665)"
fill="#07193e"
/>
<path
id="Trazado_7238"
data-name="Trazado 7238"
d="M143.562,432.083a3.239,3.239,0,0,1,.525.048v-.565a2.383,2.383,0,0,0-2.379-2.383h-15.63a2.383,2.383,0,0,0-2.379,2.383v.565a3.245,3.245,0,0,1,.525-.048Z"
transform="translate(-23.844 -372.224)"
fill="#07193e"
/>
<path
id="Trazado_7239"
data-name="Trazado 7239"
d="M122.1,482.968a2.379,2.379,0,0,0-2.379-2.379H100.381A2.379,2.379,0,0,0,98,482.968V484.3h24.1Z"
transform="translate(0 -419.924)"
fill="#07193e"
/>
</g>
</svg>
);
};
export default HardBucketQuotaIcon;

View File

@@ -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 <http://www.gnu.org/licenses/>.
import React, { SVGProps } from "react";
const ReportedUsageFullIcon = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 37.001 37"
{...props}
>
<defs>
<clipPath id="rep-quota-clip-path">
<rect
id="Rectángulo_959"
data-name="Rectángulo 959"
width="37"
height="37"
transform="translate(0 0)"
fill="#07193e"
/>
</clipPath>
</defs>
<g id="reported-usage-icn-full" transform="translate(-0.213 -0.213)">
<rect
id="Rectángulo_869"
data-name="Rectángulo 869"
width="37"
height="37"
transform="translate(0.213 0.213)"
fill="none"
/>
<g
id="Grupo_2317"
data-name="Grupo 2317"
transform="translate(0.213 0.213)"
>
<g
id="Grupo_2316"
data-name="Grupo 2316"
transform="translate(0 0)"
clipPath="url(#rep-quota-clip-path)"
>
<path
id="Trazado_7046"
data-name="Trazado 7046"
d="M18.5,0A18.5,18.5,0,1,0,37,18.5,18.5,18.5,0,0,0,18.5,0m0,18.5V4.756A13.757,13.757,0,0,1,32.238,18.5H18.5Z"
transform="translate(0.074 0.074)"
fill="#07193e"
/>
</g>
</g>
</g>
</svg>
);
};
export default ReportedUsageFullIcon;

View File

@@ -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";

View File

@@ -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<number>(0);
const [loadingPolicies, setLoadingPolicies] = useState<boolean>(true);
@@ -181,7 +190,7 @@ const AccessDetails = ({
{displayPoliciesList && <Tab label="Policies" {...a11yProps(0)} />}
{displayUsersList && <Tab label="Users" {...a11yProps(1)} />}
</Tabs>
<Paper>
<Paper className={classes.tableBlock}>
<TabPanel index={0} value={curTab}>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_LIST_USER_POLICIES]}
@@ -227,4 +236,4 @@ const AccessDetails = ({
);
};
export default connector(AccessDetails);
export default withStyles(styles)(connector(AccessDetails));

View File

@@ -34,6 +34,7 @@ import {
containerForHeader,
objectBrowserCommon,
searchField,
tableStyles,
} from "../../Common/FormComponents/common/styleLibrary";
import { BucketInfo } from "../types";
import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions";
@@ -68,6 +69,7 @@ const styles = (theme: Theme) =>
marginLeft: "10px",
align: "right",
},
...tableStyles,
...actionsTray,
...searchField,
...objectBrowserCommon,
@@ -238,7 +240,7 @@ const AccessRule = ({
</Button>
</SecureComponent>
</Grid>
<Paper>
<Paper className={classes.tableBlock}>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_BUCKET_POLICY]}
resource={bucketName}

View File

@@ -31,20 +31,15 @@ import {
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
import { setErrorSnackMessage } from "../../../../actions";
import {
setBucketDetailsLoad,
setBucketDetailsTab,
setBucketInfo,
} from "../actions";
import { setBucketDetailsLoad, setBucketInfo } from "../actions";
import { AppState } from "../../../../store";
import { ErrorResponseHandler } from "../../../../common/types";
import PageHeader from "../../Common/PageHeader/PageHeader";
import ScreenTitle from "../../Common/ScreenTitle/ScreenTitle";
import { IconButton, Tooltip } from "@mui/material";
import { Box, IconButton, Tooltip } from "@mui/material";
import RefreshIcon from "../../../../icons/RefreshIcon";
import BoxIconButton from "../../Common/BoxIconButton/BoxIconButton";
import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions";
import PageLayout from "../../Common/Layout/PageLayout";
import VerticalTabs from "../../Common/VerticalTabs/VerticalTabs";
@@ -54,9 +49,10 @@ import SecureComponent, {
} from "../../../../common/SecureComponent/SecureComponent";
import withSuspense from "../../Common/Components/withSuspense";
import RBIconButton from "./SummaryItems/RBIconButton";
import { TrashIcon } from "../../../../icons";
const BucketsIcon = React.lazy(() => 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 = ({
<PageLayout className={classes.pageContainer}>
<Grid item xs={12}>
<ScreenTitle
classes={{
screenTitle: classes.screenTitle,
}}
icon={
<Fragment>
<BucketsIcon width={40} />
@@ -251,8 +254,11 @@ const BucketDetails = ({
scopes={[IAM_SCOPES.S3_GET_BUCKET_POLICY]}
resource={bucketName}
>
Access:{" "}
<span className={classes.capitalize}>
<span style={{ fontSize: 15 }}>Access: </span>
<span
className={classes.capitalize}
style={{ fontWeight: 600, fontSize: 15 }}
>
{bucketInfo?.access.toLowerCase()}
</span>
</SecureComponent>
@@ -267,160 +273,157 @@ const BucketDetails = ({
resource={bucketName}
errorProps={{ disabled: true }}
>
<BoxIconButton
tooltip={"Delete"}
color="primary"
aria-label="Delete"
<RBIconButton
classes={{
root: classes.deleteBtn,
}}
onClick={() => {
setDeleteOpen(true);
}}
size="large"
>
<DeleteIcon />
</BoxIconButton>
text={`Delete Bucket`}
icon={<TrashIcon />}
/>
</SecureComponent>
<BoxIconButton
tooltip={"Refresh"}
color="primary"
aria-label="Refresh List"
<RBIconButton
onClick={() => {
setBucketDetailsLoad(true);
}}
size="large"
variant={"contained"}
>
<RefreshIcon />
</BoxIconButton>
text={`Refresh`}
icon={<RefreshIcon />}
color={"primary"}
/>
</Fragment>
}
/>
</Grid>
<VerticalTabs
selectedTab={activeTab}
isRouteTabs
routes={
<div className={classes.contentSpacer}>
<Router history={history}>
<Switch>
<Route
exact
path="/buckets/:bucketName/admin/summary"
component={BucketSummaryPanel}
/>
<Route
exact
path="/buckets/:bucketName/admin/events"
component={BucketEventsPanel}
/>
{distributedSetup && (
<Box sx={{ border: "1px solid #eaeaea" }}>
<VerticalTabs
selectedTab={activeTab}
isRouteTabs
routes={
<div className={classes.contentSpacer}>
<Router history={history}>
<Switch>
<Route
exact
path="/buckets/:bucketName/admin/replication"
component={BucketReplicationPanel}
path="/buckets/:bucketName/admin/summary"
component={BucketSummaryPanel}
/>
)}
{distributedSetup && (
<Route
exact
path="/buckets/:bucketName/admin/lifecycle"
component={BucketLifecyclePanel}
path="/buckets/:bucketName/admin/events"
component={BucketEventsPanel}
/>
)}
<Route
exact
path="/buckets/:bucketName/admin/access"
component={AccessDetailsPanel}
/>
<Route
exact
path="/buckets/:bucketName/admin/prefix"
component={AccessRulePanel}
/>
<Route
path="/buckets/:bucketName"
component={() => (
<Redirect to={`/buckets/${bucketName}/admin/summary`} />
{distributedSetup && (
<Route
exact
path="/buckets/:bucketName/admin/replication"
component={BucketReplicationPanel}
/>
)}
/>
</Switch>
</Router>
</div>
}
>
{{
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 && (
<Route
exact
path="/buckets/:bucketName/admin/lifecycle"
component={BucketLifecyclePanel}
/>
)}
<Route
exact
path="/buckets/:bucketName/admin/access"
component={AccessDetailsPanel}
/>
<Route
exact
path="/buckets/:bucketName/admin/prefix"
component={AccessRulePanel}
/>
<Route
path="/buckets/:bucketName"
component={() => (
<Redirect to={`/buckets/${bucketName}/admin/summary`} />
)}
/>
</Switch>
</Router>
</div>
}
>
{{
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"),
},
}}
</VerticalTabs>
to: getRoutePath("access"),
},
}}
{{
tabConfig: {
label: "Access Rules",
value: "prefix",
component: Link,
disabled: !hasPermission(bucketName, [
IAM_SCOPES.S3_GET_BUCKET_POLICY,
]),
to: getRoutePath("prefix"),
},
}}
</VerticalTabs>
</Box>
</PageLayout>
</Fragment>
);
@@ -436,7 +439,6 @@ const mapState = (state: AppState) => ({
const connector = connect(mapState, {
setErrorSnackMessage,
setBucketDetailsTab,
setBucketDetailsLoad,
setBucketInfo,
});

View File

@@ -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<boolean>(false);
const [loadingObjectLocking, setLoadingLocking] = useState<boolean>(true);
const [loadingSize, setLoadingSize] = useState<boolean>(true);
const [loadingTags, setLoadingTags] = useState<boolean>(true);
const [bucketLoading, setBucketLoading] = useState<boolean>(true);
const [loadingEncryption, setLoadingEncryption] = useState<boolean>(true);
const [loadingVersioning, setLoadingVersioning] = useState<boolean>(true);
@@ -169,11 +136,6 @@ const BucketSummary = ({
useState<boolean>(false);
const [enableVersioningOpen, setEnableVersioningOpen] =
useState<boolean>(false);
const [tags, setTags] = useState<any>(null);
const [tagModalOpen, setTagModalOpen] = useState<boolean>(false);
const [tagKeys, setTagKeys] = useState<string[]>([]);
const [selectedTag, setSelectedTag] = useState<string[]>(["", ""]);
const [deleteTagModalOpen, setDeleteTagModalOpen] = useState<boolean>(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 (
<Fragment>
@@ -506,302 +413,175 @@ const BucketSummary = ({
versioningCurrentState={isVersioned}
/>
)}
{tagModalOpen && (
<AddBucketTagModal
modalOpen={tagModalOpen}
currentTags={tags}
bucketName={bucketName}
onCloseAndUpdate={closeAddTagModal}
/>
)}
{deleteTagModalOpen && (
<DeleteBucketTagModal
deleteOpen={deleteTagModalOpen}
currentTags={tags}
bucketName={bucketName}
onCloseAndUpdate={closeDeleteTagModal}
selectedTag={selectedTag}
/>
)}
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<PanelTitle>Summary</PanelTitle>
<Grid item xs={12} className={classes.spacerBottom}>
<h3
style={{
marginTop: "0",
marginBottom: "0",
}}
>
Summary
</h3>
</Grid>
</Grid>
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={8}>
<table width={"100%"}>
<tbody>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_BUCKET_POLICY]}
resource={bucketName}
>
<tr>
<td className={classes.titleCol}>Access Policy:</td>
<td className={classes.capitalizeFirst}>
<SecureComponent
scopes={[IAM_SCOPES.S3_PUT_BUCKET_POLICY]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<Button
color="primary"
className={classes.anchorButton}
onClick={() => {
setAccessPolicyScreenOpen(true);
}}
>
{bucketLoading ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
accessPolicy.toLowerCase()
)}
</Button>
</SecureComponent>
</td>
</tr>
</SecureComponent>
{distributedSetup && (
<Fragment>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION]}
resource={bucketName}
>
<tr>
<td className={classes.titleCol}>Replication:</td>
<td className={classes.doubleElement}>
<span>
{replicationRules ? "Enabled" : "Disabled"}
</span>
</td>
</tr>
</SecureComponent>
<SecureComponent
scopes={[
IAM_SCOPES.S3_GET_BUCKET_OBJECT_LOCK_CONFIGURATION,
]}
resource={bucketName}
>
<tr>
<td className={classes.titleCol}>Object Locking:</td>
<td>{!hasObjectLocking ? "Disabled" : "Enabled"}</td>
</tr>
</SecureComponent>
</Fragment>
)}
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_BUCKET_ENCRYPTION_CONFIGURATION]}
resource={bucketName}
>
<tr>
<td className={classes.titleCol}>Encryption:</td>
<td>
{loadingEncryption ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
<SecureComponent
scopes={[
IAM_SCOPES.S3_PUT_BUCKET_ENCRYPTION_CONFIGURATION,
]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<Button
color="primary"
className={classes.anchorButton}
onClick={() => {
setEnableEncryptionScreenOpen(true);
}}
>
{encryptionEnabled ? "Enabled" : "Disabled"}
</Button>
</SecureComponent>
)}
</td>
</tr>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_BUCKET_TAGGING]}
resource={bucketName}
>
<tr>
<td className={classes.titleCol}>Tags:</td>
<td>
{tagKeys &&
tagKeys.map((tagKey: any, index: any) => {
const tag = get(tags, `${tagKey}`, "");
if (tag !== "") {
return (
<SecureComponent
key={`chip-${index}`}
scopes={[IAM_SCOPES.S3_PUT_BUCKET_TAGGING]}
resource={bucketName}
matchAll
errorProps={{
deleteIcon: null,
onDelete: null,
}}
>
<Chip
className={classes.tag}
size="small"
label={`${tagKey} : ${tag}`}
color="primary"
deleteIcon={<CloseIcon />}
onDelete={() => {
deleteTag(tagKey, tag);
}}
/>
</SecureComponent>
);
}
return null;
})}
<SecureComponent
scopes={[IAM_SCOPES.S3_PUT_BUCKET_TAGGING]}
resource={bucketName}
errorProps={{ disabled: true, onClick: null }}
>
<Chip
className={classes.tag}
icon={<AddIcon />}
clickable
size="small"
label="Add tag"
color="primary"
variant="outlined"
onClick={() => {
setTagModalOpen(true);
}}
/>
</SecureComponent>
</td>
</tr>
</SecureComponent>
</tbody>
</table>
</Grid>
<Grid item xs={4} className={classes.reportedUsage}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon} xs={2}>
<ReportedUsageIcon />
</Grid>
<Grid item xs={10}>
<Typography className={classes.elementTitle}>
Reported Usage
</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{niceBytes(bucketSize)}
</Typography>
</Grid>
</Grid>
</Paper>
<br />
<br />
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_BUCKET_POLICY]}
resource={bucketName}
>
<Box sx={{ ...twoColCssGridLayoutConfig }}>
<Box sx={{ ...twoColCssGridLayoutConfig }}>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_BUCKET_POLICY]}
resource={bucketName}
>
<EditablePropertyItem
iamScopes={[IAM_SCOPES.S3_PUT_BUCKET_POLICY]}
resourceName={bucketName}
property={"Access Policy:"}
value={accessPolicy.toLowerCase()}
onEdit={() => {
setAccessPolicyScreenOpen(true);
}}
isLoading={bucketLoading}
/>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_BUCKET_ENCRYPTION_CONFIGURATION]}
resource={bucketName}
>
<EditablePropertyItem
iamScopes={[IAM_SCOPES.S3_PUT_BUCKET_ENCRYPTION_CONFIGURATION]}
resourceName={bucketName}
property={"Encryption:"}
value={encryptionEnabled ? "Enabled" : "Disabled"}
onEdit={() => {
setEnableEncryptionScreenOpen(true);
}}
isLoading={loadingEncryption}
/>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION]}
resource={bucketName}
>
<LabelValuePair
label={"Replication:"}
value={
<LabelWithIcon
icon={replicationRules ? <EnabledIcon /> : <DisabledIcon />}
label={
<label className={classes.textMuted}>
{replicationRules ? "Enabled" : "Disabled"}
</label>
}
/>
}
/>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_BUCKET_OBJECT_LOCK_CONFIGURATION]}
resource={bucketName}
>
<LabelValuePair
label={"Object Locking:"}
value={
<LabelWithIcon
icon={hasObjectLocking ? <EnabledIcon /> : <DisabledIcon />}
label={
<label className={classes.textMuted}>
{hasObjectLocking ? "Enabled" : "Disabled"}
</label>
}
/>
}
/>
</SecureComponent>
<Box className={classes.spacerTop}>
<LabelValuePair
label={"Tags:"}
value={
<BucketTags
setErrorSnackMessage={setErrorSnackMessage}
bucketName={bucketName}
/>
}
/>
</Box>
</Box>
<Box
sx={{
display: "grid",
gridTemplateColumns: "1fr",
alignItems: "flex-start",
}}
>
<ReportedUsage bucketSize={bucketSize} />
</Box>
</Box>
</SecureComponent>
{distributedSetup && (
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_BUCKET_VERSIONING]}
resource={bucketName}
>
<Fragment>
<Paper className={classes.paperContainer} elevation={1}>
<Grid container>
<Grid item xs={quotaEnabled ? 9 : 12}>
<h2>Versioning</h2>
<hr className={classes.hrClass} />
<table width={"100%"}>
<tbody>
<tr>
<td className={classes.titleCol}>Versioning:</td>
<td>
{loadingVersioning ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
<SecureComponent
scopes={[IAM_SCOPES.S3_PUT_BUCKET_VERSIONING]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<Button
color="primary"
className={classes.anchorButton}
onClick={setBucketVersioning}
>
{isVersioned ? "Enabled" : "Disabled"}
</Button>
</SecureComponent>
)}
</td>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_GET_BUCKET_QUOTA]}
resource={bucketName}
>
<td className={classes.titleCol}>Quota:</td>
<td>
{loadingQuota ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_SET_BUCKET_QUOTA]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<Button
color="primary"
className={classes.anchorButton}
onClick={setBucketQuota}
>
{quotaEnabled ? "Enabled" : "Disabled"}
</Button>
</SecureComponent>
)}
</td>
</SecureComponent>
</tr>
</tbody>
</table>
</Grid>
{quotaEnabled && quota && (
<Grid item xs={3} className={classes.reportedUsage}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon} xs={2}>
<GavelIcon />
</Grid>
<Grid item xs={10}>
<Typography className={classes.elementTitle}>
{cap(quota?.type)} Quota
</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{niceBytes(`${quota?.quota}`)}
</Typography>
</Grid>
)}
</Grid>
</Paper>
<br />
<br />
</Fragment>
<Grid container>
<Grid item xs={12} className={classes.spacerBottom}>
<h3
style={{
marginTop: "25px",
marginBottom: "0",
}}
>
Versioning
</h3>
</Grid>
</Grid>
<Box
sx={{
...twoColCssGridLayoutConfig,
}}
>
<Box
sx={{
...twoColCssGridLayoutConfig,
}}
>
<EditablePropertyItem
iamScopes={[IAM_SCOPES.S3_PUT_BUCKET_VERSIONING]}
resourceName={bucketName}
property={"Versioning:"}
value={isVersioned ? "Enabled" : "Disabled"}
onEdit={setBucketVersioning}
isLoading={loadingVersioning}
/>
<EditablePropertyItem
iamScopes={[IAM_SCOPES.ADMIN_SET_BUCKET_QUOTA]}
resourceName={bucketName}
property={"Quota:"}
value={quotaEnabled ? "Enabled" : "Disabled"}
onEdit={setBucketQuota}
isLoading={loadingQuota}
/>
</Box>
<Box
sx={{
display: "grid",
gridTemplateColumns: "1fr",
alignItems: "flex-start",
}}
>
{quotaEnabled && quota ? <BucketQuotaSize quota={quota} /> : null}
</Box>
</Box>
</SecureComponent>
)}
@@ -810,72 +590,86 @@ const BucketSummary = ({
scopes={[IAM_SCOPES.S3_GET_OBJECT_RETENTION]}
resource={bucketName}
>
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={12}>
<h2>Retention</h2>
<hr className={classes.hrClass} />
<table width={"100%"}>
<tbody>
<tr className={classes.gridContainer}>
<td className={classes.titleCol}>Status:</td>
<td>
{loadingRetention ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
<SecureComponent
scopes={[IAM_SCOPES.S3_PUT_OBJECT_RETENTION]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<Button
color="primary"
className={classes.anchorButton}
onClick={() => {
setRetentionConfigOpen(true);
}}
>
{!retentionEnabled ? "Disabled" : "Enabled"}
</Button>
</SecureComponent>
)}
</td>
{retentionConfig === null ? (
<td colSpan={2}>&nbsp;</td>
) : (
<Fragment>
<td className={classes.titleCol}>Mode:</td>
<td className={classes.capitalizeFirst}>
{retentionConfig && retentionConfig.mode}
</td>
</Fragment>
)}
</tr>
<tr className={classes.gridContainer}>
{retentionConfig === null ? (
<td colSpan={2}></td>
) : (
<Fragment>
<td className={classes.titleCol}>Valitidy:</td>
<td className={classes.capitalizeFirst}>
{retentionConfig && retentionConfig.validity}{" "}
{retentionConfig &&
(retentionConfig.validity === 1
? retentionConfig.unit.slice(0, -1)
: retentionConfig.unit)}
</td>
</Fragment>
)}
</tr>
</tbody>
</table>
</Grid>
<Grid container>
<Grid item xs={12} className={classes.spacerBottom}>
<h3
style={{
marginTop: "25px",
marginBottom: "0",
}}
>
Retention
</h3>
</Grid>
</Paper>
</Grid>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" },
gridAutoFlow: { xs: "dense", sm: "row" } /* NEW */,
gap: 2,
}}
>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" },
gridAutoFlow: { xs: "dense", sm: "row" } /* NEW */,
gap: 2,
}}
>
<EditablePropertyItem
iamScopes={[IAM_SCOPES.ADMIN_SET_BUCKET_QUOTA]}
resourceName={bucketName}
property={"Retention:"}
value={retentionEnabled ? "Enabled" : "Disabled"}
onEdit={() => {
setRetentionConfigOpen(true);
}}
isLoading={loadingRetention}
/>
<LabelValuePair
label={"Mode:"}
value={
<label
className={classes.textMuted}
style={{ textTransform: "capitalize" }}
>
{retentionConfig && retentionConfig.mode
? retentionConfig.mode
: "-"}
</label>
}
/>
<LabelValuePair
label={"Validity:"}
value={
<label
className={classes.textMuted}
style={{ textTransform: "capitalize" }}
>
{retentionConfig && retentionConfig.validity}{" "}
{retentionConfig &&
(retentionConfig.validity === 1
? retentionConfig.unit.slice(0, -1)
: retentionConfig.unit)}
</label>
}
/>
</Box>
<Box
sx={{
display: "grid",
gridTemplateColumns: "1fr",
alignItems: "flex-start",
}}
>
{/*Spacer*/}
</Box>
</Box>
</SecureComponent>
)}
</Fragment>

View File

@@ -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 <http://www.gnu.org/licenses/>.
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 (
<Button
color="primary"
onClick={onClick}
variant="text"
sx={{
padding: 0,
margin: 0,
alignItems: "flex-start",
justifyContent: "flex-start",
display: "inline-flex",
height: "auto",
textDecoration: "underline",
color: "#2781B0",
"&:hover": {
background: "#ffffff",
textDecoration: "underline",
},
}}
disableRipple
disableFocusRipple
{...restProps}
>
{isLoading ? (
<CircularProgress color="primary" size={16} variant="indeterminate" />
) : (
label
)}
</Button>
);
};
export default ActionLink;

View File

@@ -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 <http://www.gnu.org/licenses/>.
import React from "react";
import { Box } from "@mui/material";
import { niceBytes } from "../../../../../common/utils";
import { HardBucketQuotaIcon } from "../../../../../icons";
const BucketQuotaSize = ({ quota }: { quota: any }) => {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
"& .min-icon": {
height: 37,
width: 37,
},
}}
>
<HardBucketQuotaIcon />
<Box
sx={{
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
flexFlow: "column",
marginLeft: "20px",
fontSize: "19px",
}}
>
<label
style={{
fontWeight: 600,
textTransform: "capitalize",
}}
>
{quota?.type} Quota
</label>
<label> {niceBytes(`${quota?.quota}`)}</label>
</Box>
</Box>
);
};
export default BucketQuotaSize;

View File

@@ -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 <http://www.gnu.org/licenses/>.
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<any>(null);
const [tagModalOpen, setTagModalOpen] = useState<boolean>(false);
const [tagKeys, setTagKeys] = useState<string[]>([]);
const [selectedTag, setSelectedTag] = useState<string[]>(["", ""]);
const [deleteTagModalOpen, setDeleteTagModalOpen] = useState<boolean>(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 (
<Box>
{isLoading ? (
<CircularProgress color="primary" size={16} variant="indeterminate" />
) : null}
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_BUCKET_TAGGING]}
resource={bucketName}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
}}
>
<Box>
{tagKeys &&
tagKeys.map((tagKey: any, index: any) => {
const tag = get(tags, `${tagKey}`, "");
if (tag !== "") {
return (
<SecureComponent
key={`chip-${index}`}
scopes={[IAM_SCOPES.S3_PUT_BUCKET_TAGGING]}
resource={bucketName}
matchAll
errorProps={{
deleteIcon: null,
onDelete: null,
}}
>
<Chip
style={{
textTransform: "none",
marginRight: "5px",
}}
size="small"
label={`${tagKey} : ${tag}`}
color="primary"
deleteIcon={<CloseIcon />}
onDelete={() => {
deleteTag(tagKey, tag);
}}
/>
</SecureComponent>
);
}
return null;
})}
</Box>
<SecureComponent
scopes={[IAM_SCOPES.S3_PUT_BUCKET_TAGGING]}
resource={bucketName}
errorProps={{ disabled: true, onClick: null }}
>
<Chip
style={{ maxWidth: 80, marginTop: "10px" }}
icon={<AddIcon />}
clickable
size="small"
label="Add tag"
color="primary"
variant="outlined"
onClick={() => {
setTagModalOpen(true);
}}
/>
</SecureComponent>
</Box>
</SecureComponent>
{/** Modals **/}
{tagModalOpen && (
<AddBucketTagModal
modalOpen={tagModalOpen}
currentTags={tags}
bucketName={bucketName}
onCloseAndUpdate={closeAddTagModal}
/>
)}
{deleteTagModalOpen && (
<DeleteBucketTagModal
deleteOpen={deleteTagModalOpen}
currentTags={tags}
bucketName={bucketName}
onCloseAndUpdate={closeDeleteTagModal}
selectedTag={selectedTag}
/>
)}
</Box>
);
};
export default BucketTags;

View File

@@ -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 <http://www.gnu.org/licenses/>.
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 (
<IconButton
size={"small"}
disabled={disabled}
onClick={onClick}
{...restProps}
>
<EditIcon />
</IconButton>
);
};
export default withStyles(styles)(EditActionButton);

View File

@@ -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 <http://www.gnu.org/licenses/>.
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 (
<SecureComponent
scopes={iamScopes}
resource={resourceName}
errorProps={{ disabled: true }}
>
{children}
</SecureComponent>
);
};
const EditablePropertyItem = ({
isLoading = true,
resourceName = "",
iamScopes,
property = null,
value = null,
onEdit,
}: PolicyItemProps) => {
return (
<Box
sx={{
display: "flex",
alignItems: "baseline",
justifyContent: "flex-start",
}}
>
<LabelValuePair
label={property}
value={
<SecureAction resourceName={resourceName} iamScopes={iamScopes}>
<ActionLink isLoading={isLoading} onClick={onEdit} label={value} />
</SecureAction>
}
/>
<SecureAction resourceName={resourceName} iamScopes={iamScopes}>
<EditActionButton
onClick={onEdit}
sx={{
background: "#f8f8f8",
marginLeft: "3px",
top: 3,
"& .min-icon": {
width: "16px",
height: "16px",
},
}}
/>
</SecureAction>
</Box>
);
};
export default EditablePropertyItem;

View File

@@ -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 <http://www.gnu.org/licenses/>.
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 (
<Box
sx={{
display: "flex",
}}
>
<div
style={{
height: 16,
width: 16,
display: "flex",
alignItems: "center",
marginTop: 5,
}}
>
{icon}
</div>
<div style={{ marginLeft: icon ? 5 : "none" }}>{label}</div>
</Box>
);
};
export default LabelWithIcon;

View File

@@ -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 <http://www.gnu.org/licenses/>.
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<IconButtonProps> & DeleteButtonProps) => {
return (
<BoxIconButton
classes={classes}
tooltip={tooltip || text}
variant="outlined"
onClick={onClick}
disabled={disabled}
color="secondary"
size="medium"
sx={{
border: "1px solid #f44336",
"& span": {
fontSize: 14,
"@media (max-width: 900px)": {
display: "none",
},
},
}}
{...restProps}
>
<span>{text}</span> {icon}
</BoxIconButton>
);
};
export default withStyles(styles)(RBIconButton);

View File

@@ -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 <http://www.gnu.org/licenses/>.
import React from "react";
import { Box } from "@mui/material";
import { niceBytes } from "../../../../../common/utils";
import { ReportedUsageFullIcon } from "../../../../../icons";
const ReportedUsage = ({ bucketSize }: { bucketSize: string }) => {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
"& .min-icon": {
height: 37,
width: 37,
},
}}
>
<ReportedUsageFullIcon />
<Box
sx={{
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
flexFlow: "column",
marginLeft: "20px",
fontSize: "19px",
}}
>
<label
style={{
fontWeight: 600,
}}
>
Reported Usage:
</label>
<label>{niceBytes(bucketSize)}</label>
</Box>
</Box>
);
};
export default ReportedUsage;

View File

@@ -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 = ({
<Grid item xs={12}>
<br />
</Grid>
<div {...getRootProps()}>
<div {...getRootProps({ style: { ...dndStyles } })}>
<input {...getInputProps()} />
<Grid item xs={12} className={classes.tableBlock}>
<SecureComponent
@@ -1307,8 +1345,9 @@ const ListObjects = ({
customPaperHeight={classes.browsePaper}
selectedItems={selectedObjects}
onSelect={selectListObjects}
customEmptyMessage={`This location is empty${!rewindEnabled ? ", please try uploading a new file" : ""
}`}
customEmptyMessage={`This location is empty${
!rewindEnabled ? ", please try uploading a new file" : ""
}`}
sortConfig={{
currentSort: currentSortField,
currentDirection: sortDirection,
@@ -1349,4 +1388,4 @@ const mapDispatchToProps = {
const connector = connect(mapStateToProps, mapDispatchToProps);
export default withRouter(connector(withStyles(styles)(ListObjects)));
export default withRouter(connector(withStyles(styles)(ListObjects)));

View File

@@ -154,6 +154,10 @@ const styles = (theme: Theme) =>
top: 3,
},
},
tabsContainer: {
border: "1px solid #eaeaea",
borderTop: 0,
},
...hrClass,
...buttonsStyles,
...actionsTray,
@@ -607,7 +611,11 @@ const ObjectDetails = ({
}
/>
</Grid>
<VerticalTabs>
<VerticalTabs
classes={{
tabsContainer: classes.tabsContainer,
}}
>
{{
tabConfig: {
label: "Details",

View File

@@ -1259,3 +1259,9 @@ export const modalStyleUtils: any = {
paddingTop: 10,
},
};
export const textStyleUtils: any = {
textMuted: {
color: "#8399AB",
},
};

View File

@@ -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: {