UX Bucket summary (#1355)
This commit is contained in:
committed by
GitHub
parent
eae9f46ac4
commit
b9ddadf9ce
64
portal-ui/src/icons/DisabledIcon.tsx
Normal file
64
portal-ui/src/icons/DisabledIcon.tsx
Normal 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;
|
||||
64
portal-ui/src/icons/EnabledIcon.tsx
Normal file
64
portal-ui/src/icons/EnabledIcon.tsx
Normal 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;
|
||||
83
portal-ui/src/icons/HardBucketQuotaIcon.tsx
Normal file
83
portal-ui/src/icons/HardBucketQuotaIcon.tsx
Normal 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;
|
||||
74
portal-ui/src/icons/ReportedUsageFullIcon.tsx
Normal file
74
portal-ui/src/icons/ReportedUsageFullIcon.tsx
Normal 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;
|
||||
@@ -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";
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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}> </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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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)));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1259,3 +1259,9 @@ export const modalStyleUtils: any = {
|
||||
paddingTop: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const textStyleUtils: any = {
|
||||
textMuted: {
|
||||
color: "#8399AB",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user