Interactive Bucket Naming Rules component (#2262)

This commit is contained in:
jinapurapu
2022-08-30 11:02:10 -07:00
committed by GitHub
parent 497437729b
commit 3a3a4b2fea
8 changed files with 481 additions and 87 deletions

View File

@@ -0,0 +1,54 @@
// 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 * as React from "react";
import { SVGProps } from "react";
const HideTextIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 15 15"
{...props}
>
<g id="Grupo_2449" data-name="Grupo 2449" transform="translate(-140 -181)">
<g id="OpenListIcon-full" transform="translate(144 250.612)">
<g
id="noun_chevron_2320228"
transform="translate(6.827 -63.612) rotate(90)"
>
<path
id="Trazado_6842"
data-name="Trazado 6842"
d="M.422,6.661a.433.433,0,0,1-.3-.117.37.37,0,0,1,0-.557L2.983,3.335.126.675a.37.37,0,0,1,0-.557.443.443,0,0,1,.6,0L3.889,3.052a.373.373,0,0,1,.126.274.344.344,0,0,1-.126.274L.727,6.533a.443.443,0,0,1-.306.127Z"
transform="translate(0 0)"
/>
</g>
<rect
id="Rectángulo_896"
data-name="Rectángulo 896"
width="0.462"
height="0.462"
transform="translate(0 -61.808)"
fill="none"
/>
</g>
</g>
</svg>
);
export default HideTextIcon;

View File

@@ -0,0 +1,52 @@
// 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 * as React from "react";
import { SVGProps } from "react";
const ShowTextIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 15 15"
{...props}
>
<g id="OpenListIcon-full" transform="translate(4 4.984)">
<g
id="noun_chevron_2320228"
transform="translate(0.167 4.016) rotate(-90)"
>
<path
id="Trazado_6842"
data-name="Trazado 6842"
d="M.422,0a.433.433,0,0,0-.3.117.37.37,0,0,0,0,.557L2.983,3.325.126,5.986a.37.37,0,0,0,0,.557.443.443,0,0,0,.6,0L3.889,3.609a.373.373,0,0,0,.126-.274.344.344,0,0,0-.126-.274L.727.127A.443.443,0,0,0,.422,0Z"
transform="translate(0 0)"
/>
</g>
<rect
id="Rectángulo_896"
data-name="Rectángulo 896"
width="0.462"
height="0.462"
transform="translate(0 1.75)"
fill="none"
/>
</g>
</svg>
);
export default ShowTextIcon;

View File

@@ -14,9 +14,9 @@
// 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, { Fragment, useEffect } from "react";
import React, { Fragment, useEffect, useState } from "react";
import Grid from "@mui/material/Grid";
import { Box, Button, LinearProgress } from "@mui/material";
import { Button, LinearProgress } from "@mui/material";
import { Theme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import createStyles from "@mui/styles/createStyles";
@@ -31,7 +31,10 @@ import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/
import PageHeader from "../../../Common/PageHeader/PageHeader";
import BackLink from "../../../../../common/BackLink";
import { BucketsIcon, InfoIcon } from "../../../../../icons";
import { setErrorSnackMessage } from "../../../../../systemSlice";
import { ErrorResponseHandler } from "../../../../../common/types";
import { BucketList } from "../../types";
import api from "../../../../../common/api";
import PageLayout from "../../../Common/Layout/PageLayout";
import InputUnitMenu from "../../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
import FormLayout from "../../../Common/FormLayout";
@@ -54,6 +57,7 @@ import { addBucketAsync } from "./addBucketThunks";
import AddBucketName from "./AddBucketName";
import { IAM_SCOPES } from "../../../../../common/SecureComponent/permissions";
import { hasPermission } from "../../../../../common/SecureComponent";
import BucketNamingRules from "./BucketNamingRules";
const styles = (theme: Theme) =>
createStyles({
@@ -108,6 +112,15 @@ const AddBucket = ({ classes }: IsetProps) => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const validBucketCharacters = new RegExp(`^[a-z0-9.-]*$`);
const ipAddressFormat = new RegExp(
"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(.|$)){4}$"
);
const bucketName = useSelector((state: AppState) => state.addBucket.name);
const [validationResult, setValidationResult] = useState<boolean[]>([]);
const errorList = validationResult.filter((v) => !v);
const hasErrors = errorList.length > 0;
const [records, setRecords] = useState<string[]>([]);
const versioningEnabled = useSelector(
(state: AppState) => state.addBucket.versioningEnabled
);
@@ -149,6 +162,44 @@ const AddBucket = ({ classes }: IsetProps) => {
IAM_SCOPES.S3_PUT_BUCKET_OBJECT_LOCK_CONFIGURATION,
]);
useEffect(() => {
const bucketNameErrors = [
!(bucketName.length < 3 || bucketName.length > 63),
validBucketCharacters.test(bucketName),
!(
bucketName.includes(".-") ||
bucketName.includes("-.") ||
bucketName.includes("..")
),
!ipAddressFormat.test(bucketName),
!bucketName.startsWith("xn--"),
!bucketName.endsWith("-s3alias"),
!records.includes(bucketName),
];
setValidationResult(bucketNameErrors);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bucketName]);
useEffect(() => {
const fetchRecords = () => {
api
.invoke("GET", `/api/v1/buckets`)
.then((res: BucketList) => {
var bucketList: string[] = [];
if (res.buckets != null && res.buckets.length > 0) {
res.buckets.forEach((bucket) => {
bucketList.push(bucket.name);
});
}
setRecords(bucketList);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
});
};
fetchRecords();
}, [dispatch]);
const resForm = () => {
dispatch(resetForm());
};
@@ -212,85 +263,6 @@ const AddBucket = ({ classes }: IsetProps) => {
)}
<br />
<br />
<b>Bucket Naming Rules</b>
<Box
sx={{
display: "flex",
flexFlow: "column",
fontSize: "14px",
flex: "2",
"& .step-number": {
color: "#ffffff",
height: "25px",
width: "25px",
background: "#081C42",
marginRight: "10px",
textAlign: "center",
fontWeight: 600,
borderRadius: "50%",
},
"& .step-row": {
fontSize: "14px",
display: "flex",
marginTop: "15px",
marginBottom: "2px",
"&.step-text": {
fontWeight: 400,
},
"&:before": {
content: "' '",
height: "7px",
width: "7px",
backgroundColor: "#2781B0",
marginRight: "10px",
marginTop: "7px",
flexShrink: 0,
},
},
}}
>
<Box className="step-row">
<div className="step-text">
Bucket names must be between 3 (min) and 63 (max)
characters long.
</div>
</Box>
<Box className="step-row">
<div className="step-text">
Bucket names can consist only of lowercase letters,
numbers, dots (.), and hyphens (-).
</div>
</Box>
<Box className="step-row">
<div className="step-text">
Bucket names must not contain two adjacent periods.
</div>
</Box>
<Box className="step-row">
<div className="step-text">
Bucket names must not be formatted as an IP address (for
example, 192.168.5.4).
</div>
</Box>
<Box className="step-row">
<div className="step-text">
Bucket names must not start with the prefix xn--.
</div>
</Box>
<Box className="step-row">
<div className="step-text">
Bucket names must not end with the suffix -s3alias. This
suffix is reserved for access point alias names.
</div>
</Box>
<Box className="step-row">
<div className="step-text">
Bucket names must be unique within a partition.
</div>
</Box>
</Box>
</Fragment>
}
/>
@@ -306,7 +278,10 @@ const AddBucket = ({ classes }: IsetProps) => {
>
<Grid container marginTop={1} spacing={2}>
<Grid item xs={12}>
<AddBucketName />
<AddBucketName hasErrors={hasErrors} />
</Grid>
<Grid item xs={12}>
<BucketNamingRules errorList={validationResult} />
</Grid>
<Grid item xs={12}>
<SectionTitle>Features</SectionTitle>
@@ -330,8 +305,7 @@ const AddBucket = ({ classes }: IsetProps) => {
</Fragment>
)}
</Grid>
<Grid item xs={12}>
<Grid item xs={12} spacing={2}>
{siteReplicationInfo.enabled && (
<Fragment>
<br />
@@ -498,7 +472,7 @@ const AddBucket = ({ classes }: IsetProps) => {
type="submit"
variant="contained"
color="primary"
disabled={addLoading || invalidFields.length > 0}
disabled={addLoading || invalidFields.length > 0 || hasErrors}
>
Create Bucket
</Button>

View File

@@ -20,7 +20,7 @@ import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/Inpu
import { useSelector } from "react-redux";
import { AppState, useAppDispatch } from "../../../../../store";
const AddBucketName = () => {
const AddBucketName = ({ hasErrors }: { hasErrors: boolean }) => {
const dispatch = useAppDispatch();
const bucketName = useSelector((state: AppState) => state.addBucket.name);
@@ -28,12 +28,14 @@ const AddBucketName = () => {
<InputBoxWrapper
id="bucket-name"
name="bucket-name"
error={hasErrors ? "Invalid bucket Name" : ""}
autoFocus={true}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setName(event.target.value));
}}
label="Bucket Name"
value={bucketName}
required
/>
);
};

View File

@@ -0,0 +1,150 @@
// 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, { Fragment, useState } from "react";
import Grid from "@mui/material/Grid";
import { Button, LinearProgress } from "@mui/material";
import { AppState } from "../../../../../store";
import { useSelector } from "react-redux";
import ShowTextIcon from "../../../../../icons/ShowTextIcon";
import HideTextIcon from "../../../../../icons/HideTextIcon";
import ValidRule from "./ValidRule";
import InvalidRule from "./InvalidRule";
import NARule from "./NARule";
const BucketNamingRules = ({ errorList }: { errorList: boolean[] }) => {
const lengthRuleText =
"Bucket names must be between 3 (min) and 63 (max) characters long.";
const characterRuleText =
"Bucket names can consist only of lowercase letters, numbers, dots (.), and hyphens (-).";
const periodRuleText =
"Bucket names must not contain two adjacent periods, or a period adjacent to a hyphen.";
const ipRuleText =
"Bucket names must not be formatted as an IP address (for example, 192.168.5.4).";
const prefixRuleText = "Bucket names must not start with the prefix xn--.";
const suffixRuleText =
"Bucket names must not end with the suffix -s3alias. This suffix is reserved for access point alias names.";
const uniqueRuleText = "Bucket names must be unique within a partition.";
const bucketName = useSelector((state: AppState) => state.addBucket.name);
const [showNamingRules, setShowNamingRules] = useState<boolean>(false);
const addLoading = useSelector((state: AppState) => state.addBucket.loading);
const [
lengthRule,
validCharacters,
noAdjacentPeriods,
notIPFormat,
noPrefix,
noSuffix,
uniqueName,
] = errorList;
return (
<Fragment>
<Grid item xs={12}>
{showNamingRules ? (
<span style={{ color: "#0288D1", textDecoration: "underline" }}>
{" "}
Hide Bucket Naming Rules{" "}
</span>
) : (
<span style={{ color: "#0288D1", textDecoration: "underline" }}>
View Bucket Naming Rules
</span>
)}
<Button
variant="text"
size="small"
onClick={() => {
setShowNamingRules(!showNamingRules);
}}
>
{showNamingRules ? <ShowTextIcon /> : <HideTextIcon />}
</Button>
{showNamingRules && (
<Grid container>
<Grid item xs={6}>
{bucketName.length === 0 ? (
<NARule ruleText={lengthRuleText} />
) : lengthRule ? (
<ValidRule ruleText={lengthRuleText} />
) : (
<InvalidRule ruleText={lengthRuleText} />
)}
{bucketName.length === 0 ? (
<NARule ruleText={characterRuleText} />
) : validCharacters ? (
<ValidRule ruleText={characterRuleText} />
) : (
<InvalidRule ruleText={characterRuleText} />
)}
{bucketName.length === 0 ? (
<NARule ruleText={periodRuleText} />
) : noAdjacentPeriods ? (
<ValidRule ruleText={periodRuleText} />
) : (
<InvalidRule ruleText={periodRuleText} />
)}
{bucketName.length === 0 ? (
<NARule ruleText={ipRuleText} />
) : notIPFormat ? (
<ValidRule ruleText={ipRuleText} />
) : (
<InvalidRule ruleText={ipRuleText} />
)}
</Grid>
<Grid item xs={6}>
{bucketName.length === 0 ? (
<NARule ruleText={prefixRuleText} />
) : noPrefix ? (
<ValidRule ruleText={prefixRuleText} />
) : (
<InvalidRule ruleText={prefixRuleText} />
)}
{bucketName.length === 0 ? (
<NARule ruleText={suffixRuleText} />
) : noSuffix ? (
<ValidRule ruleText={suffixRuleText} />
) : (
<InvalidRule ruleText={suffixRuleText} />
)}
{bucketName.length === 0 ? (
<NARule ruleText={uniqueRuleText} />
) : uniqueName ? (
<ValidRule ruleText={uniqueRuleText} />
) : (
<InvalidRule ruleText={uniqueRuleText} />
)}
</Grid>
</Grid>
)}
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Fragment>
);
};
export default BucketNamingRules;

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, { Fragment } from "react";
import Grid from "@mui/material/Grid";
import { ConfirmDeleteIcon } from "../../../../../icons";
interface IInvalidRule {
ruleText: string;
}
const InvalidRule = ({ ruleText }: IInvalidRule) => {
return (
<Fragment>
<Grid
container
style={{
color: "#C83B51",
display: "flex",
justifyContent: "flex-start",
}}
>
<Grid item xs={1} paddingRight={1}>
<ConfirmDeleteIcon width={"16px"} height={"16px"} />
</Grid>
<Grid
item
xs={9}
paddingLeft={1}
style={{
color: "#C83B51",
display: "flex",
justifyContent: "flex-start",
}}
>
{ruleText}
</Grid>
</Grid>
</Fragment>
);
};
export default InvalidRule;

View File

@@ -0,0 +1,53 @@
// 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, { Fragment } from "react";
import Grid from "@mui/material/Grid";
import { CircleIcon } from "../../../../../icons";
interface INARule {
ruleText: string;
}
const NARule = ({ ruleText }: INARule) => {
return (
<Fragment>
<Grid container style={{ display: "flex", justifyContent: "flex-start" }}>
<Grid item xs={1} paddingRight={1}>
<CircleIcon
width={"12px"}
height={"12px"}
style={{ color: "#8f949c" }}
/>
</Grid>
<Grid
item
xs={9}
paddingLeft={1}
style={{
color: "#8f949c",
display: "flex",
justifyContent: "flex-start",
}}
>
{ruleText}
</Grid>
</Grid>
</Fragment>
);
};
export default NARule;

View File

@@ -0,0 +1,53 @@
// 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, { Fragment } from "react";
import Grid from "@mui/material/Grid";
import { ConfirmModalIcon } from "../../../../../icons";
interface IValidRule {
ruleText: string;
}
const ValidRule = ({ ruleText }: IValidRule) => {
return (
<Fragment>
<Grid container style={{ display: "flex", justifyContent: "flex-start" }}>
<Grid item xs={1} paddingRight={1}>
<ConfirmModalIcon
width={"16px"}
height={"16px"}
style={{ color: "#18BF42" }}
/>
</Grid>
<Grid
item
xs={9}
paddingLeft={1}
style={{
color: "#8f949c",
display: "flex",
justifyContent: "flex-start",
}}
>
{ruleText}
</Grid>
</Grid>
</Fragment>
);
};
export default ValidRule;