Added lifecycle rules to multiple buckets at once support (#1566)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-02-15 10:47:28 -07:00
committed by GitHub
parent 5b2715ccc0
commit 81714bbbed
18 changed files with 1771 additions and 81 deletions

View File

@@ -20,69 +20,38 @@ import { SVGProps } from "react";
const LifecycleConfigIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="284.616"
height="49.568"
className={"min-icon"}
viewBox="0 0 256 256"
fill={"currentcolor"}
{...props}
>
<defs>
<clipPath id="clip-path">
<rect
id="Rectángulo_1035"
data-name="Rectángulo 1035"
width="210.934"
height="255.994"
fill="none"
/>
</clipPath>
<clipPath id="clip-Lifecycle_configuration">
<rect width="256" height="256" />
</clipPath>
</defs>
<g
id="Lifecycle_configuration"
data-name="Lifecycle configuration"
clipPath="url(#clip-Lifecycle_configuration)"
>
<rect width="256" height="256" fill="#fff" />
<g
id="Lifecycle_configuration_Icon"
data-name="Lifecycle configuration Icon"
>
<g
id="Grupo_2424"
data-name="Grupo 2424"
transform="translate(23.344 0.006)"
>
<g id="Grupo_2423" data-name="Grupo 2423" clipPath="url(#clip-path)">
<g transform="translate(23.344 0.006)">
<g>
<g>
<g>
<path
id="Trazado_7180"
data-name="Trazado 7180"
d="M75.518,71.671a11.964,11.964,0,0,0,16.92,0L118.06,46.047a11.963,11.963,0,0,0,0-16.92L92.437,3.5a11.964,11.964,0,0,0-16.92,16.92L82.892,27.8A104.264,104.264,0,0,0,0,129.712a11.964,11.964,0,1,0,23.928,0A80.279,80.279,0,0,1,75.4,54.879a11.959,11.959,0,0,0,.116,16.791"
transform="translate(0 0)"
d="M76.7,73.6c4.6,4.6,11.9,4.6,16.5,0l0,0l25-25c4.6-4.6,4.6-11.9,0-16.5l0,0l-25-25
c-4.6-4.6-11.9-4.6-16.5,0s-4.6,11.9,0,16.5l7.2,7.2c-47,9.9-80.8,51.3-80.8,99.4c0,6.4,5.2,11.7,11.7,11.7
s11.7-5.2,11.7-11.7c0-32.4,20-61.4,50.2-73C72.2,61.8,72.2,69.1,76.7,73.6"
/>
<path
id="Trazado_7181"
data-name="Trazado 7181"
d="M372.224,332.6a11.964,11.964,0,0,0-23.928,0,80.278,80.278,0,0,1-51.474,74.833,11.96,11.96,0,0,0-17.036-16.791l-25.623,25.623a11.914,11.914,0,0,0-3.011,5.053,12.017,12.017,0,0,0-.274,5.692,11.91,11.91,0,0,0,3.285,6.175l0,0,25.621,25.621a11.964,11.964,0,0,0,16.92-16.92l-7.374-7.375A104.262,104.262,0,0,0,372.224,332.6"
transform="translate(-161.29 -206.318)"
d="M208.8,126.8c0-6.4-5.2-11.7-11.7-11.7c-6.4,0-11.7,5.2-11.7,11.7c0,32.4-20,61.4-50.2,73
c4.5-4.6,4.4-12-0.2-16.5c-4.6-4.5-11.9-4.4-16.4,0.1l-25,25c-1.4,1.4-2.4,3.1-2.9,4.9c-0.5,1.8-0.6,3.7-0.3,5.5
c0.4,2.3,1.6,4.4,3.2,6l0,0l25,25c4.6,4.6,11.9,4.6,16.5,0s4.6-11.9,0-16.5l-7.2-7.2C175,216.3,208.7,174.9,208.8,126.8"
/>
<path
id="Trazado_7182"
data-name="Trazado 7182"
d="M225.178,323.044l6.176-4.656a24.5,24.5,0,0,0,2.824,1.184l1.1,7.65a2.82,2.82,0,0,0,2.824,2.428H249a2.827,2.827,0,0,0,2.824-2.428l1.1-7.65a25.862,25.862,0,0,0,2.824-1.184l6.178,4.628a2.822,2.822,0,0,0,3.7-.252l7.705-7.705a2.824,2.824,0,0,0,.255-3.7l-4.6-6.154a25.662,25.662,0,0,0,1.184-2.822l7.65-1.1a2.826,2.826,0,0,0,2.428-2.822V287.536a2.826,2.826,0,0,0-2.4-2.792l-7.648-1.1A26.6,26.6,0,0,0,269,280.821l4.633-6.183a2.817,2.817,0,0,0-.258-3.7l-7.7-7.762a2.814,2.814,0,0,0-3.693-.254l-6.187,4.656a25.437,25.437,0,0,0-2.823-1.184l-1.1-7.648a2.822,2.822,0,0,0-2.822-2.428H238.126a2.819,2.819,0,0,0-2.82,2.428l-1.1,7.648a25.448,25.448,0,0,0-2.824,1.184l-6.2-4.656a2.83,2.83,0,0,0-3.705.254l-7.7,7.705a2.817,2.817,0,0,0-.258,3.7l4.661,6.183a24.842,24.842,0,0,0-1.193,2.822l-7.643,1.1a2.822,2.822,0,0,0-2.429,2.823V298.4a2.822,2.822,0,0,0,2.429,2.823l7.643,1.1a24.549,24.549,0,0,0,1.193,2.823l-4.661,6.238a2.821,2.821,0,0,0,.258,3.7l7.7,7.705a2.833,2.833,0,0,0,3.705.252m9.93-30.056a8.466,8.466,0,1,1,16.931-.007v.007a8.464,8.464,0,0,1-8.46,8.468h-.008a8.465,8.465,0,0,1-8.463-8.467Z"
transform="translate(-133.14 -164.935)"
d="M92.8,157.8l6-4.5c0.9,0.4,1.8,0.8,2.8,1.2l1.1,7.5c0.2,1.4,1.4,2.4,2.8,2.4h10.6
c1.4,0,2.6-1,2.8-2.4l1.1-7.5c0.9-0.3,1.9-0.7,2.8-1.2l6,4.5c1.1,0.8,2.6,0.7,3.6-0.2l7.5-7.5c1-1,1.1-2.5,0.2-3.6l-4.5-6
c0.4-0.9,0.8-1.8,1.2-2.8l7.5-1.1c1.4-0.2,2.4-1.4,2.4-2.8v-10.7c0-1.4-1-2.5-2.3-2.7l-7.5-1.1c-0.3-0.9-0.7-1.9-1.2-2.8
l4.5-6c0.8-1.1,0.7-2.6-0.3-3.6l-7.5-7.6c-1-1-2.5-1.1-3.6-0.2l-6,4.5c-0.9-0.4-1.8-0.8-2.8-1.2l-1.1-7.5
c-0.2-1.4-1.4-2.4-2.8-2.4h-10.7c-1.4,0-2.6,1-2.7,2.4l-1.1,7.5c-0.9,0.3-1.9,0.7-2.8,1.2l-6-4.5c-1.1-0.8-2.6-0.7-3.6,0.2
l-7.5,7.5c-1,1-1.1,2.5-0.3,3.6l4.5,6c-0.4,0.9-0.8,1.8-1.2,2.8l-7.5,1.1c-1.4,0.2-2.4,1.4-2.4,2.8v10.6c0,1.4,1,2.6,2.4,2.8
l7.5,1.1c0.3,0.9,0.7,1.9,1.2,2.8l-4.5,6.1c-0.8,1.1-0.7,2.6,0.3,3.6l7.5,7.5C90.2,158.6,91.7,158.7,92.8,157.8 M102.5,128.5
c-0.1-4.6,3.6-8.3,8.2-8.3c4.6-0.1,8.3,3.6,8.3,8.2c0,0.1,0,0.1,0,0.2l0,0c0,4.6-3.7,8.3-8.2,8.3l0,0
C106.2,136.8,102.5,133.1,102.5,128.5L102.5,128.5L102.5,128.5z"
/>
</g>
</g>
<rect
id="Rectángulo_1036"
data-name="Rectángulo 1036"
width="256"
height="256"
fill="none"
/>
</g>
</g>
</svg>

View File

@@ -287,6 +287,7 @@ const BucketLifecyclePanel = ({
</Grid>
{!loadingLifecycle && (
<Grid item xs={12}>
<br />
<HelpBox
title={"Lifecycle Rules"}
iconComponent={<TiersIcon />}

View File

@@ -0,0 +1,462 @@
// 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, useEffect, useState } from "react";
import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { SelectChangeEvent, Tooltip } from "@mui/material";
import get from "lodash/get";
import Grid from "@mui/material/Grid";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
import {
createTenantCommon,
formFieldStyles,
modalStyleUtils,
spacingUtils,
} from "../../Common/FormComponents/common/styleLibrary";
import { setModalErrorSnackMessage } from "../../../../actions";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import PredefinedList from "../../Common/FormComponents/PredefinedList/PredefinedList";
import api from "../../../../common/api";
import GenericWizard from "../../Common/GenericWizard/GenericWizard";
import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import { ErrorResponseHandler } from "../../../../common/types";
import QueryMultiSelector from "../../Common/FormComponents/QueryMultiSelector/QueryMultiSelector";
import { ITiersDropDown } from "../BucketDetails/AddLifecycleModal";
import {
ITierElement,
ITierResponse,
} from "../../Configurations/TiersConfiguration/types";
import { MultiBucketResult } from "../types";
interface IBulkReplicationModal {
open: boolean;
closeModalAndRefresh: (clearSelection: boolean) => any;
classes: any;
buckets: string[];
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
}
const styles = (theme: Theme) =>
createStyles({
resultGrid: {
display: "grid",
gridTemplateColumns: "45px auto",
alignItems: "center",
justifyContent: "stretch",
},
errorIcon: {
paddingTop: 5,
color: "#C72C48",
},
successIcon: {
paddingTop: 5,
color: "#42C91A",
},
hide: {
opacity: 0,
transitionDuration: "0.3s",
},
...spacingUtils,
...modalStyleUtils,
...formFieldStyles,
...createTenantCommon,
});
const AddBulkReplicationModal = ({
open,
closeModalAndRefresh,
classes,
buckets,
setModalErrorSnackMessage,
}: IBulkReplicationModal) => {
const [addLoading, setAddLoading] = useState<boolean>(false);
const [loadingTiers, setLoadingTiers] = useState<boolean>(true);
const [tiersList, setTiersList] = useState<ITiersDropDown[]>([]);
const [prefix, setPrefix] = useState("");
const [tags, setTags] = useState<string>("");
const [storageClass, setStorageClass] = useState("");
const [NCTransitionSC, setNCTransitionSC] = useState("");
const [expiredObjectDM, setExpiredObjectDM] = useState<boolean>(false);
const [NCExpirationDays, setNCExpirationDays] = useState<string>("0");
const [NCTransitionDays, setNCTransitionDays] = useState<string>("0");
const [ilmType, setIlmType] = useState<string>("expiry");
const [expiryDays, setExpiryDays] = useState<string>("0");
const [transitionDays, setTransitionDays] = useState<string>("0");
const [isFormValid, setIsFormValid] = useState<boolean>(false);
const [results, setResults] = useState<MultiBucketResult | null>(null);
useEffect(() => {
if (loadingTiers) {
api
.invoke("GET", `/api/v1/admin/tiers`)
.then((res: ITierResponse) => {
const tiersList: ITierElement[] | null = get(res, "items", []);
if (tiersList !== null && tiersList.length >= 1) {
const objList = tiersList.map((tier: ITierElement) => {
const tierType = tier.type;
const value = get(tier, `${tierType}.name`, "");
return { label: value, value: value };
});
setTiersList(objList);
if (objList.length > 0) {
setStorageClass(objList[0].value);
}
}
setLoadingTiers(false);
})
.catch((err: ErrorResponseHandler) => {
setLoadingTiers(false);
setModalErrorSnackMessage(err);
});
}
}, [loadingTiers, setModalErrorSnackMessage]);
useEffect(() => {
let valid = true;
if (ilmType !== "expiry") {
if (storageClass === "") {
valid = false;
}
}
setIsFormValid(valid);
}, [ilmType, expiryDays, transitionDays, storageClass]);
const LogoToShow = ({ errString }: { errString: string }) => {
switch (errString) {
case "":
return (
<div className={classes.successIcon}>
<CheckCircleOutlineIcon />
</div>
);
case "n/a":
return null;
default:
if (errString) {
return (
<div className={classes.errorIcon}>
<Tooltip title={errString} placement="top-start">
<ErrorOutlineIcon />
</Tooltip>
</div>
);
}
}
return null;
};
const createLifecycleRules = (to: any) => {
let rules = {};
if (ilmType === "expiry") {
let expiry = {
expiry_days: parseInt(expiryDays),
};
rules = {
...expiry,
noncurrentversion_expiration_days: parseInt(NCExpirationDays),
};
} else {
let transition = {
transition_days: parseInt(transitionDays),
};
rules = {
...transition,
noncurrentversion_transition_days: parseInt(NCTransitionDays),
noncurrentversion_transition_storage_class: NCTransitionSC,
storage_class: storageClass,
};
}
const lifecycleInsert = {
buckets,
type: ilmType,
prefix,
tags,
expired_object_delete_marker: expiredObjectDM,
...rules,
};
api
.invoke("POST", `/api/v1/buckets/multi-lifecycle`, lifecycleInsert)
.then((res: MultiBucketResult) => {
setAddLoading(false);
setResults(res);
to("++");
})
.catch((err: ErrorResponseHandler) => {
setAddLoading(false);
setModalErrorSnackMessage(err);
});
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh(false);
}}
title="Set Lifecycle to multiple buckets"
>
<GenericWizard
loadingStep={addLoading || loadingTiers}
wizardSteps={[
{
label: "Lifecycle Configuration",
componentRender: (
<Fragment>
<Grid item xs={12}>
<PredefinedList
label="Local Buckets to replicate"
content={buckets.join(", ")}
/>
</Grid>
<h4>Remote Endpoint Configuration</h4>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
<Grid item xs={12} className={classes.formFieldRow}>
<fieldset className={classes.fieldGroup}>
<legend className={classes.descriptionText}>
Lifecycle Configuration
</legend>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={ilmType}
id="quota_type"
name="quota_type"
label="ILM Rule"
onChange={(
e: React.ChangeEvent<{ value: unknown }>
) => {
setIlmType(e.target.value as string);
}}
selectorOptions={[
{ value: "expiry", label: "Expiry" },
{ value: "transition", label: "Transition" },
]}
/>
</Grid>
{ilmType === "expiry" ? (
<Fragment>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
type="number"
id="expiry_days"
name="expiry_days"
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
setExpiryDays(e.target.value);
}}
label="Expiry Days"
value={expiryDays}
min="0"
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
type="number"
id="noncurrentversion_expiration_days"
name="noncurrentversion_expiration_days"
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
setNCExpirationDays(e.target.value);
}}
label="Non-current Expiration Days"
value={NCExpirationDays}
min="0"
/>
</Grid>
</Fragment>
) : (
<Fragment>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
type="number"
id="transition_days"
name="transition_days"
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
setTransitionDays(e.target.value);
}}
label="Transition Days"
value={transitionDays}
min="0"
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
type="number"
id="noncurrentversion_transition_days"
name="noncurrentversion_transition_days"
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
setNCTransitionDays(e.target.value);
}}
label="Non-current Transition Days"
value={NCTransitionDays}
min="0"
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="noncurrentversion_t_SC"
name="noncurrentversion_t_SC"
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
setNCTransitionSC(e.target.value);
}}
placeholder="Set Non-current Version Transition Storage Class"
label="Non-current Version Transition Storage Class"
value={NCTransitionSC}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<SelectWrapper
label="Storage Class"
id="storage_class"
name="storage_class"
value={storageClass}
onChange={(e: SelectChangeEvent<string>) => {
setStorageClass(e.target.value as string);
}}
options={tiersList}
/>
</Grid>
</Fragment>
)}
</fieldset>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<fieldset className={classes.fieldGroup}>
<legend className={classes.descriptionText}>
File Configuration
</legend>
<Grid item xs={12}>
<InputBoxWrapper
id="prefix"
name="prefix"
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
setPrefix(e.target.value);
}}
label="Prefix"
value={prefix}
/>
</Grid>
<Grid item xs={12}>
<QueryMultiSelector
name="tags"
label="Tags"
elements={tags}
onChange={(vl: string) => {
setTags(vl);
}}
keyPlaceholder="Tag Key"
valuePlaceholder="Tag Value"
withBorder
/>
</Grid>
<Grid item xs={12}>
<FormSwitchWrapper
value="expired_delete_marker"
id="expired_delete_marker"
name="expired_delete_marker"
checked={expiredObjectDM}
onChange={(
event: React.ChangeEvent<HTMLInputElement>
) => {
setExpiredObjectDM(event.target.checked);
}}
label={"Expired Object Delete Marker"}
/>
</Grid>
</fieldset>
</Grid>
</Grid>
</Grid>
</Fragment>
),
buttons: [
{
type: "custom",
label: "Create Rules",
enabled: !loadingTiers && !addLoading && isFormValid,
action: createLifecycleRules,
},
],
},
{
label: "Results",
componentRender: (
<Fragment>
<h3>Multi Bucket lifecycle Assignments Results</h3>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
<h4>Buckets Results</h4>
{results?.results.map((resultItem) => {
return (
<div className={classes.resultGrid}>
{LogoToShow({ errString: resultItem.error || "" })}
<span>{resultItem.bucketName}</span>
</div>
);
})}
</Grid>
</Grid>
</Fragment>
),
buttons: [
{
type: "custom",
label: "Done",
enabled: !addLoading,
action: () => closeModalAndRefresh(true),
},
],
},
]}
forModal
/>
</ModalWrapper>
);
};
const connector = connect(null, {
setModalErrorSnackMessage,
});
export default withStyles(styles)(connector(AddBulkReplicationModal));

View File

@@ -22,7 +22,7 @@ import withStyles from "@mui/styles/withStyles";
import { LinearProgress } from "@mui/material";
import Grid from "@mui/material/Grid";
import { Bucket, BucketList } from "../types";
import { AddIcon, BucketsIcon } from "../../../../icons";
import {AddIcon, BucketsIcon, LifecycleConfigIcon} from "../../../../icons";
import { AppState } from "../../../../store";
import { setErrorSnackMessage } from "../../../../actions";
import {
@@ -50,6 +50,7 @@ import PageLayout from "../../Common/Layout/PageLayout";
import SearchBox from "../../Common/SearchBox";
import VirtualizedList from "../../Common/VirtualizedList/VirtualizedList";
import RBIconButton from "../BucketDetails/SummaryItems/RBIconButton";
import BulkLifecycleModal from "./BulkLifecycleModal";
const styles = (theme: Theme) =>
createStyles({
@@ -100,6 +101,7 @@ const ListBuckets = ({
const [selectedBuckets, setSelectedBuckets] = useState<string[]>([]);
const [replicationModalOpen, setReplicationModalOpen] =
useState<boolean>(false);
const [lifecycleModalOpen, setLifecycleModalOpen] = useState<boolean>(false);
const [bulkSelect, setBulkSelect] = useState<boolean>(false);
@@ -174,6 +176,14 @@ const ListBuckets = ({
}
};
const closeBulkLifecycleModal = (unselectAll: boolean) => {
setLifecycleModalOpen(false);
if (unselectAll) {
setSelectedBuckets([]);
}
};
const renderItemLine = (index: number) => {
const bucket = filteredRecords[index] || null;
if (bucket) {
@@ -213,6 +223,13 @@ const ListBuckets = ({
closeModalAndRefresh={closeBulkReplicationModal}
/>
)}
{lifecycleModalOpen && (
<BulkLifecycleModal
buckets={selectedBuckets}
closeModalAndRefresh={closeBulkLifecycleModal}
open={lifecycleModalOpen}
/>
)}
<PageHeader label={"Buckets"} />
<PageLayout>
<Grid item xs={12} className={classes.actionsTray} display="flex">
@@ -241,6 +258,18 @@ const ListBuckets = ({
variant={bulkSelect ? "contained" : "outlined"}
/>
<RBIconButton
tooltip={"Set Lifecycle"}
onClick={() => {
setLifecycleModalOpen(true);
}}
text={""}
icon={<LifecycleConfigIcon />}
disabled={selectedBuckets.length === 0}
color={"primary"}
variant={"outlined"}
/>
<RBIconButton
tooltip={"Set Replication"}
onClick={() => {

View File

@@ -201,3 +201,12 @@ export interface LifeCycleItem {
tags?: any;
status?: string;
}
export interface MultiBucketResult {
bucketName: string,
error?: string,
}
export interface MultiBucketResult {
results: MultiBucketResult[],
}