Consolidate Remote Buckets and Replication modals (#317)
* Consolidate Remote Buckets and Replication modals This commit consolidates Remote Buckets and Replication modals into a single modal in the Add Replication Rule modal located in the Buckets section Fixes #301 * Remove Remote Buckets section * Properly align tabs and button on Buckets page
This commit is contained in:
@@ -25,11 +25,7 @@ import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBo
|
||||
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
|
||||
import { Button, LinearProgress } from "@material-ui/core";
|
||||
import api from "../../../../common/api";
|
||||
import {
|
||||
IRemoteBucket,
|
||||
IRemoteBucketsResponse,
|
||||
} from "../../RemoteBuckets/types";
|
||||
import RemoteBucketsList from "../../RemoteBuckets/RemoteBuckets";
|
||||
import { IRemoteBucket } from "../types";
|
||||
|
||||
interface IReplicationModal {
|
||||
open: boolean;
|
||||
@@ -64,42 +60,61 @@ const AddReplicationModal = ({
|
||||
bucketName,
|
||||
}: IReplicationModal) => {
|
||||
const [addError, setAddError] = useState("");
|
||||
const [loadingForm, setLoadingForm] = useState(true);
|
||||
const [addLoading, setAddLoading] = useState(false);
|
||||
const [remoteURL, setRemoteURL] = useState("");
|
||||
const [source, setSource] = useState("");
|
||||
const [target, setTarget] = useState("");
|
||||
const [ARN, setARN] = useState("");
|
||||
const [arnValues, setARNValues] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (addLoading) {
|
||||
addRecord();
|
||||
}
|
||||
}, [addLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingForm) {
|
||||
getARNValues();
|
||||
}
|
||||
});
|
||||
const [accessKey, setAccessKey] = useState("");
|
||||
const [secretKey, setSecretKey] = useState("");
|
||||
const [targetURL, setTargetURL] = useState("");
|
||||
const [targetBucket, setTargetBucket] = useState("");
|
||||
const [region, setRegion] = useState("");
|
||||
|
||||
const addRecord = () => {
|
||||
const replicationInfo = {
|
||||
destination_bucket: target,
|
||||
arn: ARN,
|
||||
const remoteBucketInfo = {
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
sourceBucket: bucketName,
|
||||
targetURL: targetURL,
|
||||
targetBucket: targetBucket,
|
||||
region: region,
|
||||
};
|
||||
|
||||
api
|
||||
.invoke(
|
||||
"POST",
|
||||
`/api/v1/buckets/${bucketName}/replication`,
|
||||
replicationInfo
|
||||
)
|
||||
.then((res) => {
|
||||
setAddLoading(false);
|
||||
setAddError("");
|
||||
closeModalAndRefresh();
|
||||
.invoke("POST", "/api/v1/remote-buckets", remoteBucketInfo)
|
||||
.then(() => {
|
||||
api
|
||||
.invoke("GET", "/api/v1/remote-buckets")
|
||||
.then((res: any) => {
|
||||
const remoteBuckets = get(res, "buckets", []);
|
||||
const remoteBucket = remoteBuckets.find(
|
||||
(itemRemote: IRemoteBucket) => {
|
||||
return itemRemote.sourceBucket === bucketName;
|
||||
}
|
||||
);
|
||||
if (remoteBucket && remoteBucket.remoteARN) {
|
||||
const remoteARN = remoteBucket.remoteARN;
|
||||
const replicationInfo = {
|
||||
destination_bucket: targetBucket,
|
||||
arn: remoteARN,
|
||||
};
|
||||
api
|
||||
.invoke(
|
||||
"POST",
|
||||
`/api/v1/buckets/${bucketName}/replication`,
|
||||
replicationInfo
|
||||
)
|
||||
.then(() => {
|
||||
setAddLoading(false);
|
||||
setAddError("");
|
||||
closeModalAndRefresh();
|
||||
})
|
||||
.catch((err) => {
|
||||
setAddLoading(false);
|
||||
setAddError(err);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setAddError(err);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
setAddLoading(false);
|
||||
@@ -107,23 +122,6 @@ const AddReplicationModal = ({
|
||||
});
|
||||
};
|
||||
|
||||
const getARNValues = () => {
|
||||
api
|
||||
.invoke("GET", "/api/v1/remote-buckets")
|
||||
.then((res: any) => {
|
||||
const remoteBuckets = get(res, "buckets", []);
|
||||
|
||||
const remoteARNS = remoteBuckets.map((itemRemote: IRemoteBucket) => {
|
||||
return { label: itemRemote.remoteARN, value: itemRemote.remoteARN };
|
||||
});
|
||||
setLoadingForm(false);
|
||||
setARNValues(remoteARNS);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoadingForm(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalWrapper
|
||||
modalOpen={open}
|
||||
@@ -139,71 +137,96 @@ const AddReplicationModal = ({
|
||||
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setAddLoading(true);
|
||||
addRecord();
|
||||
}}
|
||||
>
|
||||
{loadingForm && (
|
||||
<Grid container>
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="target"
|
||||
name="target"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setAccessKey(e.target.value);
|
||||
}}
|
||||
label="Access Key"
|
||||
value={accessKey}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="target"
|
||||
name="target"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSecretKey(e.target.value);
|
||||
}}
|
||||
label="Secret Key"
|
||||
value={secretKey}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="target"
|
||||
name="target"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTargetURL(e.target.value);
|
||||
}}
|
||||
placeholder="https://play.min.io:9000"
|
||||
label="Target URL"
|
||||
value={targetURL}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="target"
|
||||
name="target"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTargetBucket(e.target.value);
|
||||
}}
|
||||
label="Target Bucket"
|
||||
value={targetBucket}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="target"
|
||||
name="target"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRegion(e.target.value);
|
||||
}}
|
||||
label="Region"
|
||||
value={region}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={addLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Grid>
|
||||
{addLoading && (
|
||||
<Grid item xs={12}>
|
||||
<LinearProgress />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
{!loadingForm && (
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="target"
|
||||
name="target"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTarget(e.target.value);
|
||||
}}
|
||||
label="Destination Bucket"
|
||||
value={target}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<SelectWrapper
|
||||
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
|
||||
setARN(e.target.value as string);
|
||||
}}
|
||||
id="arn"
|
||||
name="arn"
|
||||
label={"ARN"}
|
||||
value={ARN}
|
||||
options={arnValues}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={addLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Grid>
|
||||
{addLoading && (
|
||||
<Grid item xs={12}>
|
||||
<LinearProgress />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
)}
|
||||
</Grid>
|
||||
</form>
|
||||
</ModalWrapper>
|
||||
);
|
||||
|
||||
@@ -515,51 +515,53 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Tabs
|
||||
value={curTab}
|
||||
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
|
||||
this.setState({ curTab: newValue });
|
||||
}}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
aria-label="cluster-tabs"
|
||||
>
|
||||
<Tab label="Events" {...a11yProps(0)} />
|
||||
<Tab label="Replication" {...a11yProps(1)} />
|
||||
</Tabs>
|
||||
</Grid>
|
||||
<Grid item xs={6} className={classes.actionsTray}>
|
||||
{curTab === 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<CreateIcon />}
|
||||
size="medium"
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
addScreenOpen: true,
|
||||
});
|
||||
<Grid container item xs={12}>
|
||||
<Grid item xs={6}>
|
||||
<Tabs
|
||||
value={curTab}
|
||||
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
|
||||
this.setState({ curTab: newValue });
|
||||
}}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
aria-label="cluster-tabs"
|
||||
>
|
||||
Subscribe to Event
|
||||
</Button>
|
||||
)}
|
||||
{curTab === 1 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<CreateIcon />}
|
||||
size="medium"
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
openSetReplication: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add Replication Rule
|
||||
</Button>
|
||||
)}
|
||||
<Tab label="Events" {...a11yProps(0)} />
|
||||
<Tab label="Replication" {...a11yProps(1)} />
|
||||
</Tabs>
|
||||
</Grid>
|
||||
<Grid item xs={6} className={classes.actionsTray}>
|
||||
{curTab === 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<CreateIcon />}
|
||||
size="medium"
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
addScreenOpen: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Subscribe to Event
|
||||
</Button>
|
||||
)}
|
||||
{curTab === 1 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<CreateIcon />}
|
||||
size="medium"
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
openSetReplication: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add Replication Rule
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TabPanel index={0} value={curTab}>
|
||||
|
||||
@@ -79,3 +79,15 @@ export interface MakeBucketRequest {
|
||||
versioning: boolean;
|
||||
quota?: QuotaRequest;
|
||||
}
|
||||
|
||||
export interface IRemoteBucket {
|
||||
name: string;
|
||||
accessKey: string;
|
||||
secretKey: string;
|
||||
sourceBucket: string;
|
||||
targetURL: string;
|
||||
targetBucket: string;
|
||||
remoteARN: string;
|
||||
status: string;
|
||||
service: string;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,6 @@ import { ISessionResponse } from "./types";
|
||||
import { saveSessionResponse } from "./actions";
|
||||
import TenantDetails from "./Tenants/TenantDetails/TenantDetails";
|
||||
import { clearSession } from "../../common/utils";
|
||||
import RemoteBuckets from "./RemoteBuckets/RemoteBuckets";
|
||||
import ObjectBrowser from "./ObjectBrowser/ObjectBrowser";
|
||||
import ListObjects from "./Buckets/ListBuckets/Objects/ListObjects/ListObjects";
|
||||
|
||||
@@ -279,10 +278,6 @@ const Console = ({
|
||||
component: Policies,
|
||||
path: "/policies",
|
||||
},
|
||||
{
|
||||
component: RemoteBuckets,
|
||||
path: "/remote-buckets",
|
||||
},
|
||||
{
|
||||
component: Trace,
|
||||
path: "/trace",
|
||||
|
||||
@@ -21,7 +21,6 @@ import ListItem from "@material-ui/core/ListItem";
|
||||
import ListItemIcon from "@material-ui/core/ListItemIcon";
|
||||
import WebAssetIcon from "@material-ui/icons/WebAsset";
|
||||
import HealingIcon from "@material-ui/icons/Healing";
|
||||
import CloudUploadIcon from "@material-ui/icons/CloudUpload";
|
||||
import DescriptionIcon from "@material-ui/icons/Description";
|
||||
import FileCopyIcon from "@material-ui/icons/FileCopy";
|
||||
import Collapse from "@material-ui/core/Collapse";
|
||||
@@ -247,14 +246,6 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
|
||||
name: "IAM Policies",
|
||||
icon: <IAMPoliciesIcon />,
|
||||
},
|
||||
{
|
||||
group: "Admin",
|
||||
type: "item",
|
||||
component: NavLink,
|
||||
to: "/remote-buckets",
|
||||
name: "Remote Buckets",
|
||||
icon: <CloudUploadIcon />,
|
||||
},
|
||||
{
|
||||
group: "Tools",
|
||||
type: "item",
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 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, { useState, useEffect } from "react";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { Button, LinearProgress } from "@material-ui/core";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
|
||||
import api from "../../../common/api";
|
||||
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
import SelectWrapper from "../Common/FormComponents/SelectWrapper/SelectWrapper";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red",
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right",
|
||||
},
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
interface IAddBucketProps {
|
||||
classes: any;
|
||||
open: boolean;
|
||||
closeModalAndRefresh: () => void;
|
||||
}
|
||||
|
||||
const AddRemoteBucket = ({
|
||||
classes,
|
||||
open,
|
||||
closeModalAndRefresh,
|
||||
}: IAddBucketProps) => {
|
||||
const [addLoading, setAddLoading] = useState(false);
|
||||
const [addError, setAddError] = useState("");
|
||||
const [accessKey, setAccessKey] = useState("");
|
||||
const [secretKey, setSecretKey] = useState("");
|
||||
const [sourceBucket, setSourceBucket] = useState("");
|
||||
const [targetURL, setTargetURL] = useState("");
|
||||
const [targetBucket, setTargetBucket] = useState("");
|
||||
const [region, setRegion] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (addLoading) {
|
||||
addRecord();
|
||||
}
|
||||
}, [addLoading]);
|
||||
|
||||
const addRecord = () => {
|
||||
const remoteBucketInfo = {
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
sourceBucket: sourceBucket,
|
||||
targetURL: targetURL,
|
||||
targetBucket: targetBucket,
|
||||
region: region,
|
||||
};
|
||||
|
||||
api
|
||||
.invoke("POST", "/api/v1/remote-buckets", remoteBucketInfo)
|
||||
.then((res) => {
|
||||
setAddLoading(false);
|
||||
setAddError("");
|
||||
closeModalAndRefresh();
|
||||
})
|
||||
.catch((err) => {
|
||||
setAddLoading(false);
|
||||
setAddError(err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalWrapper
|
||||
title="Create Remote Bucket"
|
||||
modalOpen={open}
|
||||
onClose={() => {
|
||||
setAddError("");
|
||||
closeModalAndRefresh();
|
||||
}}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<form
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
addRecord();
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{addError}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="accessKey"
|
||||
name="accessKey"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setAccessKey(e.target.value);
|
||||
}}
|
||||
label="Access Key"
|
||||
value={accessKey}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="secretKey"
|
||||
name="secretKey"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSecretKey(e.target.value);
|
||||
}}
|
||||
label="Secret Key"
|
||||
value={secretKey}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="sourceBucket"
|
||||
name="sourceBucket"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSourceBucket(e.target.value);
|
||||
}}
|
||||
label="Source Bucket"
|
||||
value={sourceBucket}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="targetURL"
|
||||
name="targetURL"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTargetURL(e.target.value);
|
||||
}}
|
||||
placeholder="https://play.min.io:9000"
|
||||
label="Target URL"
|
||||
value={targetURL}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="targetBucket"
|
||||
name="targetBucket"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTargetBucket(e.target.value);
|
||||
}}
|
||||
label="Target Bucket"
|
||||
value={targetBucket}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<InputBoxWrapper
|
||||
id="region"
|
||||
name="region"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRegion(e.target.value);
|
||||
}}
|
||||
label="Region"
|
||||
value={region}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={addLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Grid>
|
||||
{addLoading && (
|
||||
<Grid item xs={12}>
|
||||
<LinearProgress />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</form>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(AddRemoteBucket);
|
||||
@@ -1,137 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 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, { useState, useEffect } from "react";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import get from "lodash/get";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
LinearProgress,
|
||||
} from "@material-ui/core";
|
||||
import api from "../../../common/api";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red",
|
||||
},
|
||||
});
|
||||
|
||||
interface IDeleteEventProps {
|
||||
classes: any;
|
||||
closeDeleteModalAndRefresh: (refresh: boolean) => void;
|
||||
deleteOpen: boolean;
|
||||
bucketName: any;
|
||||
sourceBucket: string;
|
||||
}
|
||||
|
||||
interface IDeleteEventState {
|
||||
deleteLoading: boolean;
|
||||
deleteError: string;
|
||||
}
|
||||
|
||||
const DeleteRemoteBucket = ({
|
||||
deleteOpen,
|
||||
closeDeleteModalAndRefresh,
|
||||
classes,
|
||||
bucketName,
|
||||
sourceBucket,
|
||||
}: IDeleteEventProps) => {
|
||||
const [deleteError, setDeleteError] = useState("");
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (deleteLoading) {
|
||||
removeRecord();
|
||||
}
|
||||
}, [deleteLoading]);
|
||||
|
||||
const removeRecord = () => {
|
||||
api
|
||||
.invoke("DELETE", `/api/v1/remote-buckets/${sourceBucket}/${bucketName}`)
|
||||
.then(() => {
|
||||
setDeleteLoading(false);
|
||||
setDeleteError("");
|
||||
closeDeleteModalAndRefresh(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
setDeleteLoading(false);
|
||||
setDeleteError(err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={deleteOpen}
|
||||
onClose={() => {
|
||||
setDeleteError("");
|
||||
closeDeleteModalAndRefresh(false);
|
||||
}}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">Delete Remote Bucket</DialogTitle>
|
||||
<DialogContent>
|
||||
{deleteLoading && <LinearProgress />}
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Are you sure you want to delete <strong>'{bucketName}'</strong> Remote
|
||||
Bucket?
|
||||
{deleteError !== "" && (
|
||||
<React.Fragment>
|
||||
<br />
|
||||
<Typography
|
||||
component="p"
|
||||
variant="body1"
|
||||
className={classes.errorBlock}
|
||||
>
|
||||
{deleteError}
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDeleteError("");
|
||||
closeDeleteModalAndRefresh(false);
|
||||
}}
|
||||
color="primary"
|
||||
disabled={deleteLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDeleteLoading(true);
|
||||
}}
|
||||
color="secondary"
|
||||
autoFocus
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(DeleteRemoteBucket);
|
||||
@@ -1,273 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 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, { useState, useEffect } from "react";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import { Button } from "@material-ui/core";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import InputAdornment from "@material-ui/core/InputAdornment";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import Moment from "react-moment";
|
||||
import api from "../../../common/api";
|
||||
import { Bucket } from "../Buckets/types";
|
||||
import TableWrapper from "../Common/TableWrapper/TableWrapper";
|
||||
import AddRemoteBucket from "./AddRemoteBucket";
|
||||
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
|
||||
import { CreateIcon } from "../../../icons";
|
||||
import { IRemoteBucket, IRemoteBucketsResponse } from "./types";
|
||||
import DeleteRemoteBucket from "./DeleteRemoteBucket";
|
||||
import {
|
||||
actionsTray,
|
||||
containerForHeader,
|
||||
searchField,
|
||||
} from "../Common/FormComponents/common/styleLibrary";
|
||||
import PageHeader from "../Common/PageHeader/PageHeader";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
seeMore: {
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
paper: {
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column",
|
||||
},
|
||||
|
||||
addSideBar: {
|
||||
width: "320px",
|
||||
padding: "20px",
|
||||
},
|
||||
errorBlock: {
|
||||
color: "red",
|
||||
},
|
||||
tableToolbar: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(0),
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
"& tr": {
|
||||
"& th": {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
...actionsTray,
|
||||
...searchField,
|
||||
...containerForHeader(theme.spacing(4)),
|
||||
});
|
||||
|
||||
interface IRemoteListBucketsProps {
|
||||
classes: any;
|
||||
}
|
||||
|
||||
const RemoteBucketsList = ({ classes }: IRemoteListBucketsProps) => {
|
||||
const [records, setRecords] = useState<IRemoteBucket[]>([]);
|
||||
const [totalRecords, setTotalRecords] = useState<number>(0);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
|
||||
const [deleteScreenOpen, setDeleteOpen] = useState<boolean>(false);
|
||||
const [page, setPage] = useState<number>(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
|
||||
const [selectedBucket, setSelectedBucket] = useState<IRemoteBucket>({
|
||||
remoteARN: "",
|
||||
accessKey: "",
|
||||
name: "",
|
||||
secretKey: "",
|
||||
service: "",
|
||||
sourceBucket: "",
|
||||
status: "",
|
||||
targetBucket: "",
|
||||
targetURL: "",
|
||||
});
|
||||
const [filterBuckets, setFilterBuckets] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
fetchRecords();
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
const closeAddModalAndRefresh = () => {
|
||||
setAddScreenOpen(false);
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
const closeDeleteModalAndRefresh = (reload: boolean) => {
|
||||
setDeleteOpen(false);
|
||||
|
||||
if (reload) {
|
||||
setLoading(true);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRecords = () => {
|
||||
const offset = page * rowsPerPage;
|
||||
api
|
||||
.invoke("GET", `/api/v1/remote-buckets`)
|
||||
.then((res: IRemoteBucketsResponse) => {
|
||||
setLoading(false);
|
||||
setRecords(res.buckets || []);
|
||||
setTotalRecords(!res.buckets ? 0 : res.total);
|
||||
setError("");
|
||||
// if we get 0 results, and page > 0 , go down 1 page
|
||||
if (
|
||||
(res.buckets === undefined ||
|
||||
res.buckets == null ||
|
||||
res.buckets.length === 0) &&
|
||||
page > 0
|
||||
) {
|
||||
const newPage = page - 1;
|
||||
setPage(newPage);
|
||||
setLoading(true);
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setLoading(false);
|
||||
setError(err);
|
||||
});
|
||||
};
|
||||
|
||||
const offset = page * rowsPerPage;
|
||||
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const rPP = parseInt(event.target.value, 10);
|
||||
setPage(0);
|
||||
setRowsPerPage(rPP);
|
||||
};
|
||||
|
||||
const confirmDeleteRemoteBucket = (arnRemoteBucket: IRemoteBucket) => {
|
||||
setSelectedBucket(arnRemoteBucket);
|
||||
setDeleteOpen(true);
|
||||
};
|
||||
|
||||
const tableActions = [{ type: "delete", onClick: confirmDeleteRemoteBucket }];
|
||||
|
||||
const filteredRecords = records
|
||||
.slice(offset, offset + rowsPerPage)
|
||||
.filter((b: IRemoteBucket) => {
|
||||
if (filterBuckets === "") {
|
||||
return true;
|
||||
} else {
|
||||
if (b.name.indexOf(filterBuckets) >= 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{addScreenOpen && (
|
||||
<AddRemoteBucket
|
||||
open={addScreenOpen}
|
||||
closeModalAndRefresh={() => {
|
||||
closeAddModalAndRefresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{deleteScreenOpen && (
|
||||
<DeleteRemoteBucket
|
||||
bucketName={selectedBucket.remoteARN}
|
||||
sourceBucket={selectedBucket.sourceBucket}
|
||||
closeDeleteModalAndRefresh={(reload) => {
|
||||
closeDeleteModalAndRefresh(reload);
|
||||
}}
|
||||
deleteOpen={deleteScreenOpen}
|
||||
/>
|
||||
)}
|
||||
<PageHeader label="Remote Buckets" />
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.container}>
|
||||
<Grid item xs={12} className={classes.actionsTray}>
|
||||
<TextField
|
||||
placeholder="Search Remote Buckets"
|
||||
className={classes.searchField}
|
||||
id="search-resource"
|
||||
label=""
|
||||
onChange={(val) => {
|
||||
setFilterBuckets(val.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<CreateIcon />}
|
||||
onClick={() => {
|
||||
setAddScreenOpen(true);
|
||||
}}
|
||||
>
|
||||
Create Remote Bucket
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TableWrapper
|
||||
itemActions={tableActions}
|
||||
columns={[
|
||||
{ label: "Remote ARN", elementKey: "remoteARN" },
|
||||
{ label: "Source Bucket", elementKey: "sourceBucket" },
|
||||
{ label: "Target Bucket", elementKey: "targetBucket" },
|
||||
{ label: "Status", elementKey: "status" },
|
||||
]}
|
||||
isLoading={loading}
|
||||
records={filteredRecords}
|
||||
entityName="Remote Buckets"
|
||||
idField="remoteARN"
|
||||
paginatorConfig={{
|
||||
rowsPerPageOptions: [5, 10, 25],
|
||||
colSpan: 3,
|
||||
count: totalRecords,
|
||||
rowsPerPage: rowsPerPage,
|
||||
page: page,
|
||||
SelectProps: {
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true,
|
||||
},
|
||||
onChangePage: handleChangePage,
|
||||
onChangeRowsPerPage: handleChangeRowsPerPage,
|
||||
ActionsComponent: MinTablePaginationActions,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(RemoteBucketsList);
|
||||
@@ -1,32 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 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/>.
|
||||
|
||||
export interface IRemoteBucketsResponse {
|
||||
buckets: IRemoteBucket[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface IRemoteBucket {
|
||||
name: string;
|
||||
accessKey: string;
|
||||
secretKey: string;
|
||||
sourceBucket: string;
|
||||
targetURL: string;
|
||||
targetBucket: string;
|
||||
remoteARN: string;
|
||||
status: string;
|
||||
service: string;
|
||||
}
|
||||
Reference in New Issue
Block a user