Deprecated Site Replication in UI (#3469)

* Deprecated Sire Replication in UI

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

* fix-workflow-issue

---------

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2024-11-11 14:26:19 -06:00
committed by GitHub
parent 18e50975d4
commit 4e5dcf0fc3
53 changed files with 15 additions and 6455 deletions

View File

@@ -16,15 +16,11 @@
import { useEffect, useState } from "react";
import { Navigate, useLocation } from "react-router-dom";
import useApi from "./screens/Console/Common/Hooks/useApi";
import { ErrorResponseHandler } from "./common/types";
import { ReplicationSite } from "./screens/Console/Configurations/SiteReplication/SiteReplication";
import { useSelector } from "react-redux";
import { SRInfoStateType } from "./types";
import { AppState, useAppDispatch } from "./store";
import LoadingComponent from "./common/LoadingComponent";
import { fetchSession } from "./screens/LoginPage/sessionThunk";
import { setSiteReplicationInfo, setLocationPath } from "./systemSlice";
import { setLocationPath } from "./systemSlice";
import { SessionCallStates } from "./screens/Console/consoleSlice.types";
interface ProtectedRouteProps {
@@ -39,9 +35,6 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
const sessionLoadingState = useSelector(
(state: AppState) => state.console.sessionLoadingState,
);
const anonymousMode = useSelector(
(state: AppState) => state.system.anonymousMode,
);
const { pathname = "" } = useLocation();
const StorePathAndRedirect = () => {
@@ -63,40 +56,6 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
}
}, [dispatch, sessionLoadingState]);
const [, invokeSRInfoApi] = useApi(
(res: any) => {
const { name: curSiteName, enabled = false } = res || {};
let siteList = res.site;
if (!siteList) {
siteList = [];
}
const isSiteNameInList = siteList.find((si: ReplicationSite) => {
return si.name === curSiteName;
});
const isCurSite = enabled && isSiteNameInList;
const siteReplicationDetail: SRInfoStateType = {
enabled: enabled,
curSite: isCurSite,
siteName: isCurSite ? curSiteName : "",
};
dispatch(setSiteReplicationInfo(siteReplicationDetail));
},
(err: ErrorResponseHandler) => {
// we will fail this call silently, but show it on the console
console.error(`Error loading site replication status`, err);
},
);
useEffect(() => {
if (userLoggedIn && !componentLoading && !anonymousMode) {
invokeSRInfoApi("GET", `api/v1/admin/site-replication`);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userLoggedIn, componentLoading]);
// if we're still trying to retrieve user session render nothing
if (componentLoading) {
return <LoadingComponent />;

View File

@@ -4073,134 +4073,6 @@ export class Api<
...params,
}),
/**
* No description
*
* @tags SiteReplication
* @name GetSiteReplicationInfo
* @summary Get list of Replication Sites
* @request GET:/admin/site-replication
* @secure
*/
getSiteReplicationInfo: (params: RequestParams = {}) =>
this.request<SiteReplicationInfoResponse, ApiError>({
path: `/admin/site-replication`,
method: "GET",
secure: true,
format: "json",
...params,
}),
/**
* No description
*
* @tags SiteReplication
* @name SiteReplicationInfoAdd
* @summary Add a Replication Site
* @request POST:/admin/site-replication
* @secure
*/
siteReplicationInfoAdd: (
body: SiteReplicationAddRequest,
params: RequestParams = {},
) =>
this.request<SiteReplicationAddResponse, ApiError>({
path: `/admin/site-replication`,
method: "POST",
body: body,
secure: true,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* No description
*
* @tags SiteReplication
* @name SiteReplicationEdit
* @summary Edit a Replication Site
* @request PUT:/admin/site-replication
* @secure
*/
siteReplicationEdit: (body: PeerInfo, params: RequestParams = {}) =>
this.request<PeerSiteEditResponse, ApiError>({
path: `/admin/site-replication`,
method: "PUT",
body: body,
secure: true,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* No description
*
* @tags SiteReplication
* @name SiteReplicationRemove
* @summary Remove a Replication Site
* @request DELETE:/admin/site-replication
* @secure
*/
siteReplicationRemove: (body: PeerInfoRemove, params: RequestParams = {}) =>
this.request<PeerSiteRemoveResponse, ApiError>({
path: `/admin/site-replication`,
method: "DELETE",
body: body,
secure: true,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* No description
*
* @tags SiteReplication
* @name GetSiteReplicationStatus
* @summary Display overall site replication status
* @request GET:/admin/site-replication/status
* @secure
*/
getSiteReplicationStatus: (
query?: {
/**
* Include Bucket stats
* @default true
*/
buckets?: boolean;
/**
* Include Group stats
* @default true
*/
groups?: boolean;
/**
* Include Policies stats
* @default true
*/
policies?: boolean;
/**
* Include Policies stats
* @default true
*/
users?: boolean;
/** Entity Type to lookup */
entityType?: string;
/** Entity Value to lookup */
entityValue?: string;
},
params: RequestParams = {},
) =>
this.request<SiteReplicationStatusResponse, ApiError>({
path: `/admin/site-replication/status`,
method: "GET",
query: query,
secure: true,
format: "json",
...params,
}),
/**
* No description
*

View File

@@ -197,9 +197,6 @@ export const IAM_PAGES = {
TIERS: "/settings/tiers",
TIERS_ADD: "/settings/tiers/add",
TIERS_ADD_SERVICE: "/settings/tiers/add/:service",
SITE_REPLICATION: "/settings/site-replication",
SITE_REPLICATION_STATUS: "/settings/site-replication/status",
SITE_REPLICATION_ADD: "/settings/site-replication/add",
};
// roles
@@ -391,18 +388,6 @@ export const IAM_PAGES_PERMISSIONS = {
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.SITE_REPLICATION]: [
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.SITE_REPLICATION_STATUS]: [
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.SITE_REPLICATION_ADD]: [
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.KMS]: [IAM_SCOPES.KMS_ALL_ACTIONS],
[IAM_PAGES.KMS_STATUS]: [IAM_SCOPES.KMS_ALL_ACTIONS, IAM_SCOPES.KMS_STATUS],
[IAM_PAGES.KMS_KEYS]: [

View File

@@ -1,624 +0,0 @@
// 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 { useNavigate } from "react-router-dom";
import {
BackLink,
Button,
ClustersIcon,
HelpBox,
PageLayout,
Box,
Grid,
ProgressBar,
InputLabel,
SectionTitle,
} from "mds";
import useApi from "../../Common/Hooks/useApi";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
import {
setErrorSnackMessage,
setHelpName,
setSnackBarMessage,
} from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { useSelector } from "react-redux";
import { selSession } from "../../consoleSlice";
import SRSiteInputRow from "./SRSiteInputRow";
import { SiteInputRow } from "./Types";
import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../../HelpMenu";
const isValidEndPoint = (ep: string) => {
let isValidEndPointUrl = false;
try {
new URL(ep);
isValidEndPointUrl = true;
} catch (err) {
isValidEndPointUrl = false;
}
if (isValidEndPointUrl) {
return "";
} else {
return "Invalid Endpoint";
}
};
const isEmptyValue = (value: string): boolean => {
return value?.trim() === "";
};
const TableHeader = () => {
return (
<React.Fragment>
<Box>
<InputLabel>Site Name</InputLabel>
</Box>
<Box>
<InputLabel>Endpoint {"*"}</InputLabel>
</Box>
<Box>
<InputLabel>Access Key {"*"}</InputLabel>
</Box>
<Box>
<InputLabel>Secret Key {"*"}</InputLabel>
</Box>
<Box> </Box>
</React.Fragment>
);
};
const SiteTypeHeader = ({ title }: { title: string }) => {
return (
<Grid item xs={12}>
<Box
sx={{
marginBottom: "15px",
fontSize: "14px",
fontWeight: 600,
}}
>
{title}
</Box>
</Grid>
);
};
const AddReplicationSites = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { serverEndPoint = "" } = useSelector(selSession);
const [currentSite, setCurrentSite] = useState<SiteInputRow[]>([
{
endpoint: serverEndPoint,
name: "",
accessKey: "",
secretKey: "",
},
]);
const [existingSites, setExistingSites] = useState<SiteInputRow[]>([]);
const setDefaultNewRows = () => {
const defaultNewSites = [
{ endpoint: "", name: "", accessKey: "", secretKey: "" },
];
setExistingSites(defaultNewSites);
};
const [isSiteInfoLoading, invokeSiteInfoApi] = useApi(
(res: any) => {
const { sites: siteList, name: curSiteName } = res;
// current site name to be the fist one.
const foundIdx = siteList.findIndex((el: any) => el.name === curSiteName);
if (foundIdx !== -1) {
let curSite = siteList[foundIdx];
curSite = {
...curSite,
isCurrent: true,
isSaved: true,
};
setCurrentSite([curSite]);
siteList.splice(foundIdx, 1);
}
siteList.sort((x: any, y: any) => {
return x.name === curSiteName ? -1 : y.name === curSiteName ? 1 : 0;
});
let existingSiteList = siteList.map((si: any) => {
return {
...si,
accessKey: "",
secretKey: "",
isSaved: true,
};
});
if (existingSiteList.length) {
setExistingSites(existingSiteList);
} else {
setDefaultNewRows();
}
},
(err: any) => {
setDefaultNewRows();
},
);
const getSites = () => {
invokeSiteInfoApi("GET", `api/v1/admin/site-replication`);
};
useEffect(() => {
getSites();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
dispatch(setHelpName("add-replication-sites"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const existingEndPointsValidity = existingSites.reduce(
(acc: string[], cv, i) => {
const epValue = existingSites[i].endpoint;
const isEpValid = isValidEndPoint(epValue);
if (isEpValid === "" && epValue !== "") {
acc.push(isEpValid);
}
return acc;
},
[],
);
const isExistingCredsValidity = existingSites
.map((site) => {
return !isEmptyValue(site.accessKey) && !isEmptyValue(site.secretKey);
})
.filter(Boolean);
const { accessKey: cAccessKey, secretKey: cSecretKey } = currentSite[0];
const isCurCredsValid =
!isEmptyValue(cAccessKey) && !isEmptyValue(cSecretKey);
const peerEndpointsValid =
existingEndPointsValidity.length === existingSites.length;
const peerCredsValid =
isExistingCredsValidity.length === existingSites.length;
let isAllFieldsValid =
isCurCredsValid && peerEndpointsValid && peerCredsValid;
const [isAdding, invokeSiteAddApi] = useApi(
(res: any) => {
if (res.success) {
dispatch(setSnackBarMessage(res.status));
resetForm();
getSites();
navigate(IAM_PAGES.SITE_REPLICATION);
} else {
dispatch(
setErrorSnackMessage({
errorMessage: "Error",
detailedError: res.status,
}),
);
}
},
(err: any) => {
dispatch(setErrorSnackMessage(err));
},
);
const resetForm = () => {
setDefaultNewRows();
setCurrentSite((prevItems) => {
return prevItems.map((item, ix) => ({
...item,
accessKey: "",
secretKey: "",
name: "",
}));
});
};
const addSiteReplication = () => {
const curSite: any[] = currentSite?.map((es, idx) => {
return {
accessKey: es.accessKey,
secretKey: es.secretKey,
name: es.name,
endpoint: es.endpoint.trim(),
};
});
const newOrExistingSitesToAdd = existingSites.reduce(
(acc: any, ns, idx) => {
if (ns.endpoint) {
acc.push({
accessKey: ns.accessKey,
secretKey: ns.secretKey,
name: ns.name || `dr-site-${idx}`,
endpoint: ns.endpoint.trim(),
});
}
return acc;
},
[],
);
const sitesToAdd = curSite.concat(newOrExistingSitesToAdd);
invokeSiteAddApi("POST", `api/v1/admin/site-replication`, sitesToAdd);
};
const renderCurrentSite = () => {
return (
<Box
sx={{
marginTop: "15px",
}}
>
<SiteTypeHeader title={"This Site"} />
<Box
withBorders
sx={{
display: "grid",
gridTemplateColumns: ".8fr 1.2fr .8fr .8fr .2fr",
padding: "15px",
gap: "10px",
maxHeight: "430px",
overflowY: "auto",
}}
>
<TableHeader />
{currentSite.map((cs, index) => {
const accessKeyError = isEmptyValue(cs.accessKey)
? "AccessKey is required"
: "";
const secretKeyError = isEmptyValue(cs.secretKey)
? "SecretKey is required"
: "";
return (
<SRSiteInputRow
key={`current-${index}`}
rowData={cs}
rowId={index}
fieldErrors={{
accessKey: accessKeyError,
secretKey: secretKeyError,
}}
onFieldChange={(e, fieldName, index) => {
const filedValue = e.target.value;
if (fieldName !== "") {
setCurrentSite((prevItems) => {
return prevItems.map((item, ix) =>
ix === index
? { ...item, [fieldName]: filedValue }
: item,
);
});
}
}}
showRowActions={false}
/>
);
})}
</Box>
</Box>
);
};
const renderPeerSites = () => {
return (
<Box
sx={{
marginTop: "25px",
}}
>
<SiteTypeHeader title={"Peer Sites"} />
<Box
withBorders
sx={{
display: "grid",
gridTemplateColumns: ".8fr 1.2fr .8fr .8fr .2fr",
padding: "15px",
gap: "10px",
maxHeight: "430px",
overflowY: "auto",
}}
>
<TableHeader />
{existingSites.map((ps, index) => {
const endPointError = isValidEndPoint(ps.endpoint);
const accessKeyError = isEmptyValue(ps.accessKey)
? "AccessKey is required"
: "";
const secretKeyError = isEmptyValue(ps.secretKey)
? "SecretKey is required"
: "";
return (
<SRSiteInputRow
key={`exiting-${index}`}
rowData={ps}
rowId={index}
fieldErrors={{
endpoint: endPointError,
accessKey: accessKeyError,
secretKey: secretKeyError,
}}
onFieldChange={(e, fieldName, index) => {
const filedValue = e.target.value;
setExistingSites((prevItems) => {
return prevItems.map((item, ix) =>
ix === index
? { ...item, [fieldName]: filedValue }
: item,
);
});
}}
canAdd={true}
canRemove={index > 0 && !ps.isSaved}
onAddClick={() => {
const newRows = [...existingSites];
//add at the next index
newRows.splice(index + 1, 0, {
name: "",
endpoint: "",
accessKey: "",
secretKey: "",
});
setExistingSites(newRows);
}}
onRemoveClick={(index) => {
setExistingSites(
existingSites.filter((_, idx) => idx !== index),
);
}}
/>
);
})}
</Box>
</Box>
);
};
return (
<Fragment>
<PageHeaderWrapper
label={
<BackLink
label={"Add Replication Site"}
onClick={() => navigate(IAM_PAGES.SITE_REPLICATION)}
/>
}
actions={<HelpMenu />}
/>
<PageLayout>
<Box
sx={{
display: "grid",
padding: "25px",
gap: "25px",
gridTemplateColumns: "1fr",
border: "1px solid #eaeaea",
}}
>
<Box>
<SectionTitle separator icon={<ClustersIcon />}>
Add Sites for Replication
</SectionTitle>
{isSiteInfoLoading || isAdding ? <ProgressBar /> : null}
<Box
sx={{
fontSize: "14px",
fontStyle: "italic",
marginTop: "10px",
marginBottom: "10px",
}}
>
Note: AccessKey and SecretKey values for every site is required
while adding or editing peer sites
</Box>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
return addSiteReplication();
}}
>
{renderCurrentSite()}
{renderPeerSites()}
<Grid item xs={12}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginTop: "20px",
gap: "15px",
}}
>
<Button
id={"clear"}
type="button"
variant="regular"
disabled={isAdding}
onClick={resetForm}
label={"Clear"}
/>
<Button
id={"save"}
type="submit"
variant="callAction"
disabled={isAdding || !isAllFieldsValid}
label={"Save"}
/>
</Box>
</Grid>
</form>
</Box>
<HelpBox
title={""}
iconComponent={null}
help={
<Fragment>
<Box
sx={{
marginTop: "-25px",
fontSize: "16px",
fontWeight: 600,
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
padding: "2px",
}}
>
<Box
sx={{
backgroundColor: "#07193E",
height: "15px",
width: "15px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "50%",
marginRight: "18px",
padding: "3px",
paddingLeft: "2px",
"& .min-icon": {
height: "11px",
width: "11px",
fill: "#ffffff",
},
}}
>
<ClustersIcon />
</Box>
About Site Replication
</Box>
<Box
sx={{
display: "flex",
flexFlow: "column",
fontSize: "14px",
flex: "2",
"& li": {
fontSize: "14px",
display: "flex",
marginTop: "15px",
marginBottom: "15px",
width: "100%",
"&.step-text": {
fontWeight: 400,
},
},
}}
>
<Box>
The following changes are replicated to all other sites
</Box>
<ul>
<li>Creation and deletion of buckets and objects</li>
<li>
Creation and deletion of all IAM users, groups, policies
and their mappings to users or groups
</li>
<li>Creation of STS credentials</li>
<li>
Creation and deletion of service accounts (except those
owned by the root user)
</li>
<li>
<Box
style={{
display: "flex",
flexFlow: "column",
justifyContent: "flex-start",
}}
>
<div
style={{
paddingTop: "1px",
}}
>
Changes to Bucket features such as
</div>
<ul>
<li>Bucket Policies</li>
<li>Bucket Tags</li>
<li>Bucket Object-Lock configurations</li>
<li>Bucket Encryption configuration</li>
</ul>
</Box>
</li>
<li>
<Box
style={{
display: "flex",
flexFlow: "column",
justifyContent: "flex-start",
}}
>
<div
style={{
paddingTop: "1px",
}}
>
The following Bucket features will NOT be replicated
</div>
<ul>
<li>Bucket notification configuration</li>
<li>Bucket lifecycle (ILM) configuration</li>
</ul>
</Box>
</li>
</ul>
</Box>
</Fragment>
}
/>
</Box>
</PageLayout>
</Fragment>
);
};
export default AddReplicationSites;

View File

@@ -1,162 +0,0 @@
// 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, { useState } from "react";
import { Box, Button, EditIcon, Grid, InputBox, InputLabel } from "mds";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import useApi from "../../Common/Hooks/useApi";
import {
setErrorSnackMessage,
setSnackBarMessage,
} from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
import styled from "styled-components";
import get from "lodash/get";
const SiteEndpointContainer = styled.div(({ theme }) => ({
"& .alertText": {
color: get(theme, "signalColors.danger", "#C51B3F"),
},
}));
const EditSiteEndPoint = ({
editSite = {},
onClose,
onComplete,
}: {
editSite: any;
onClose: () => void;
onComplete: () => void;
}) => {
const dispatch = useAppDispatch();
const [editEndPointName, setEditEndPointName] = useState<string>("");
const [isEditing, invokeSiteEditApi] = useApi(
(res: any) => {
if (res.success) {
dispatch(setSnackBarMessage(res.status));
} else {
dispatch(
setErrorSnackMessage({
errorMessage: "Error",
detailedError: res.status,
}),
);
}
onComplete();
},
(err: any) => {
dispatch(setErrorSnackMessage(err));
onComplete();
},
);
const updatePeerSite = () => {
invokeSiteEditApi("PUT", `api/v1/admin/site-replication`, {
endpoint: editEndPointName,
name: editSite.name,
deploymentId: editSite.deploymentID, // readonly
});
};
let isValidEndPointUrl = false;
try {
new URL(editEndPointName);
isValidEndPointUrl = true;
} catch (err) {
isValidEndPointUrl = false;
}
return (
<ModalWrapper
title={`Edit Replication Endpoint `}
modalOpen={true}
titleIcon={<EditIcon />}
onClose={onClose}
>
<SiteEndpointContainer>
<Box
sx={{
display: "flex",
flexFlow: "column",
marginBottom: "15px",
}}
>
<Box sx={{ marginBottom: "10px" }}>
<strong>Site:</strong> {" "}
{editSite.name}
</Box>
<Box sx={{ marginBottom: "10px" }}>
<strong>Current Endpoint:</strong> {" "}
{editSite.endpoint}
</Box>
</Box>
<Grid item xs={12}>
<InputLabel sx={{ marginBottom: 5 }}>New Endpoint:</InputLabel>
<InputBox
id="edit-rep-peer-endpoint"
name="edit-rep-peer-endpoint"
placeholder={"https://dr.minio-storage:9000"}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setEditEndPointName(event.target.value);
}}
label=""
value={editEndPointName}
/>
</Grid>
<Grid
item
xs={12}
sx={{
marginBottom: 15,
fontStyle: "italic",
display: "flex",
alignItems: "center",
fontSize: "12px",
marginTop: 2,
}}
>
<strong>Note:</strong>&nbsp;
<span className={"alertText"}>
Access Key and Secret Key should be same on the new site/endpoint.
</span>
</Grid>
</SiteEndpointContainer>
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"close"}
type="button"
variant="regular"
onClick={onClose}
label={"Cancel"}
/>
<Button
id={"update"}
type="button"
variant="callAction"
disabled={isEditing || !isValidEndPointUrl}
onClick={updatePeerSite}
label={"Update"}
/>
</Grid>
</ModalWrapper>
);
};
export default EditSiteEndPoint;

View File

@@ -1,220 +0,0 @@
// 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, { useState } from "react";
import {
Box,
breakPoints,
Button,
ClustersIcon,
Grid,
Loader,
Select,
InputBox,
} from "mds";
import useApi from "../../Common/Hooks/useApi";
import { StatsResponseType } from "./SiteReplicationStatus";
import BucketEntityStatus from "./LookupStatus/BucketEntityStatus";
import PolicyEntityStatus from "./LookupStatus/PolicyEntityStatus";
import GroupEntityStatus from "./LookupStatus/GroupEntityStatus";
import UserEntityStatus from "./LookupStatus/UserEntityStatus";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
const EntityReplicationLookup = () => {
const [entityType, setEntityType] = useState<string>("bucket");
const [entityValue, setEntityValue] = useState<string>("");
const [stats, setStats] = useState<StatsResponseType>({});
const [statsLoaded, setStatsLoaded] = useState<boolean>(false);
const [isStatsLoading, invokeSiteStatsApi] = useApi(
(res: any) => {
setStats(res);
setStatsLoaded(true);
},
(err: any) => {
setStats({});
setStatsLoaded(true);
},
);
const {
bucketStats = {},
sites = {},
userStats = {},
policyStats = {},
groupStats = {},
} = stats || {};
const getStats = (entityType: string = "", entityValue: string = "") => {
setStatsLoaded(false);
if (entityType && entityValue) {
let url = `api/v1/admin/site-replication/status?buckets=false&entityType=${entityType}&entityValue=${entityValue}&groups=false&policies=false&users=false`;
invokeSiteStatsApi("GET", url);
}
};
return (
<Box>
<Box
sx={{
display: "grid",
alignItems: "center",
gridTemplateColumns: ".7fr .9fr 1.2fr .3fr",
[`@media (max-width: ${breakPoints.sm}px)`]: {
gridTemplateColumns: "1fr",
},
[`@media (max-width: ${breakPoints.md}px)`]: {
gridTemplateColumns: "1.2fr .7fr .7fr .3fr",
},
gap: "15px",
}}
>
<Box sx={{ width: "240px", flexGrow: "0" }}>
View Replication Status for a:
</Box>
<Box
sx={{
marginLeft: -25,
[`@media (max-width: ${breakPoints.sm}px)`]: {
marginLeft: 0,
},
}}
>
<Select
id="replicationEntityLookup"
name="replicationEntityLookup"
onChange={(value) => {
setEntityType(value);
setStatsLoaded(false);
}}
label=""
value={entityType}
options={[
{
label: "Bucket",
value: "bucket",
},
{
label: "User",
value: "user",
},
{
label: "Group",
value: "group",
},
{
label: "Policy",
value: "policy",
},
]}
disabled={false}
/>
</Box>
<Box
sx={{
flex: 2,
}}
>
<InputBox
id="replicationLookupEntityValue"
name="replicationLookupEntityValue"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setEntityValue(e.target.value);
setStatsLoaded(false);
}}
placeholder={`test-${entityType}`}
label=""
value={entityValue}
/>
</Box>
<Box
sx={{
maxWidth: "80px",
}}
>
<TooltipWrapper tooltip={"View across sites"}>
<Button
id={"view-across-sites"}
type={"button"}
onClick={() => {
getStats(entityType, entityValue);
}}
label={`View`}
icon={<ClustersIcon />}
collapseOnSmall={false}
disabled={!entityValue || !entityType}
/>
</TooltipWrapper>
</Box>
</Box>
{isStatsLoading ? (
<Grid
item
xs={12}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
marginTop: 45,
}}
>
<Loader style={{ width: 25, height: 25 }} />
</Grid>
) : null}
{statsLoaded ? (
<Box>
{!isStatsLoading && entityType === "bucket" && entityValue ? (
<BucketEntityStatus
bucketStats={bucketStats}
sites={sites}
lookupValue={entityValue}
/>
) : null}
{!isStatsLoading && entityType === "user" && entityValue ? (
<UserEntityStatus
userStats={userStats}
sites={sites}
lookupValue={entityValue}
/>
) : null}
{!isStatsLoading && entityType === "group" && entityValue ? (
<GroupEntityStatus
groupStats={groupStats}
sites={sites}
lookupValue={entityValue}
/>
) : null}
{!isStatsLoading && entityType === "policy" && entityValue ? (
<PolicyEntityStatus
policyStats={policyStats}
sites={sites}
lookupValue={entityValue}
/>
) : null}
</Box>
) : null}
</Box>
);
};
export default EntityReplicationLookup;

View File

@@ -1,129 +0,0 @@
// 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 { StatsResponseType } from "../SiteReplicationStatus";
import LookupStatusTable from "./LookupStatusTable";
import { EntityNotFound, isEntityNotFound, syncStatus } from "./Utils";
type BucketEntityStatusProps = Partial<StatsResponseType> & {
lookupValue?: string;
};
const BucketEntityStatus = ({
bucketStats = {},
sites = {},
lookupValue = "",
}: BucketEntityStatusProps) => {
const rowsForStatus = [
"Tags",
"Policy",
"Quota",
"Retention",
"Encryption",
"Replication",
];
const bucketSites: Record<string, any> = bucketStats[lookupValue] || {};
if (!lookupValue) return null;
const siteKeys = Object.keys(sites);
const notFound = isEntityNotFound(sites, bucketSites, "HasBucket");
const resultMatrix: any = [];
if (notFound) {
return <EntityNotFound entityType={"Bucket"} entityValue={lookupValue} />;
} else {
const row = [];
for (let sCol = 0; sCol < siteKeys.length; sCol++) {
if (sCol === 0) {
row.push("");
}
/**
* ----------------------------------
* | <blank cell> | sit-0 | site-1 |
* -----------------------------------
*/
row.push(sites[siteKeys[sCol]].name);
}
resultMatrix.push(row);
for (let fi = 0; fi < rowsForStatus.length; fi++) {
/**
* -------------------------------------------------
* | Feature Name | site-0-status | site-1-status |
* --------------------------------------------------
*/
const sfRow = [];
const feature = rowsForStatus[fi];
let sbStatus: string | boolean = "";
for (let si = 0; si < siteKeys.length; si++) {
const bucketSiteDeploymentId = sites[siteKeys[si]].deploymentID;
const rSite = bucketSites[bucketSiteDeploymentId];
if (si === 0) {
sfRow.push(feature);
}
switch (fi) {
case 0:
sbStatus = syncStatus(rSite.TagMismatch, rSite.HasTagsSet);
sfRow.push(sbStatus);
break;
case 1:
sbStatus = syncStatus(rSite.PolicyMismatch, rSite.HasPolicySet);
sfRow.push(sbStatus);
break;
case 2:
sbStatus = syncStatus(rSite.QuotaCfgMismatch, rSite.HasQuotaCfgSet);
sfRow.push(sbStatus);
break;
case 3:
sbStatus = syncStatus(
rSite.OLockConfigMismatch,
rSite.HasOLockConfigSet,
);
sfRow.push(sbStatus);
break;
case 4:
sbStatus = syncStatus(rSite.SSEConfigMismatch, rSite.HasSSECfgSet);
sfRow.push(sbStatus);
break;
case 5:
sbStatus = syncStatus(
rSite.ReplicationCfgMismatch,
rSite.HasReplicationCfg,
);
sfRow.push(sbStatus);
break;
}
}
resultMatrix.push(sfRow);
}
}
return (
<LookupStatusTable
matrixData={resultMatrix}
entityName={lookupValue}
entityType={"Bucket"}
/>
);
};
export default BucketEntityStatus;

View File

@@ -1,99 +0,0 @@
// 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 { StatsResponseType } from "../SiteReplicationStatus";
import LookupStatusTable from "./LookupStatusTable";
import { EntityNotFound, isEntityNotFound, syncStatus } from "./Utils";
type GroupEntityStatusProps = Partial<StatsResponseType> & {
lookupValue?: string;
};
const UserEntityStatus = ({
groupStats = {},
sites = {},
lookupValue = "",
}: GroupEntityStatusProps) => {
const rowsForStatus = ["Info", "Policy mapping"];
const groupSites: Record<string, any> = groupStats[lookupValue] || {};
if (!lookupValue) return null;
const siteKeys = Object.keys(sites);
const notFound = isEntityNotFound(sites, groupSites, "HasGroup");
const resultMatrix: any = [];
if (notFound) {
return <EntityNotFound entityType={"Group"} entityValue={lookupValue} />;
} else {
const row = [];
for (let sCol = 0; sCol < siteKeys.length; sCol++) {
if (sCol === 0) {
row.push("");
}
/**
* ----------------------------------
* | <blank cell> | sit-0 | site-1 |
* -----------------------------------
*/
row.push(sites[siteKeys[sCol]].name);
}
resultMatrix.push(row);
for (let fi = 0; fi < rowsForStatus.length; fi++) {
/**
* -------------------------------------------------
* | Feature Name | site-0-status | site-1-status |
* --------------------------------------------------
*/
const sfRow = [];
const feature = rowsForStatus[fi];
let sbStatus: string | boolean = "";
for (let si = 0; si < siteKeys.length; si++) {
const bucketSiteDeploymentId = sites[siteKeys[si]].deploymentID;
const rSite = groupSites[bucketSiteDeploymentId];
if (si === 0) {
sfRow.push(feature);
}
switch (fi) {
case 0:
sbStatus = syncStatus(rSite.GroupDescMismatch, rSite.HasGroup);
sfRow.push(sbStatus);
break;
case 1:
sbStatus = syncStatus(rSite.PolicyMismatch, rSite.HasPolicyMapping);
sfRow.push(sbStatus);
break;
}
}
resultMatrix.push(sfRow);
}
}
return (
<LookupStatusTable
matrixData={resultMatrix}
entityName={lookupValue}
entityType={"Group"}
/>
);
};
export default UserEntityStatus;

View File

@@ -1,142 +0,0 @@
// 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 styled from "styled-components";
import get from "lodash/get";
import { Box, CircleIcon } from "mds";
const LookupTableBase = styled.div(({ theme }) => ({
marginTop: 15,
table: {
width: "100%",
borderCollapse: "collapse",
"& .feature-cell": {
fontWeight: 600,
fontSize: 14,
paddingLeft: 15,
},
"& .status-cell": {
textAlign: "center",
},
"& .header-cell": {
textAlign: "center",
},
"& tr": {
height: 38,
"& td": {
borderBottom: `1px solid ${get(theme, "borderColor", "#E2E2E2")}`,
},
"& th": {
borderBottom: `2px solid ${get(theme, "borderColor", "#E2E2E2")}`,
},
},
"& .indicator": {
display: "flex",
alignItems: "center",
justifyContent: "center",
"& .min-icon": {
height: 15,
width: 15,
},
"&.active": {
"& .min-icon": {
fill: get(theme, "signalColors.good", "#4CCB92"),
},
},
"&.deactivated": {
"& .min-icon": {
fill: get(theme, "signalColors.danger", "#C51B3F"),
},
},
},
},
}));
const LookupStatusTable = ({
matrixData = [],
entityName = "",
entityType = "",
}: {
matrixData: any;
entityName: string;
entityType: string;
}) => {
//Assumes 1st row should be a header row.
const [header = [], ...rows] = matrixData;
const tableHeader = header.map((hC: string, hcIdx: number) => {
return (
<th className="header-cell" key={`${0}${hcIdx}`}>
{hC}
</th>
);
});
const tableRowsToRender = rows.map((r: any, rIdx: number) => {
return (
<tr key={`r-${rIdx + 1}`}>
{r.map((v: any, cIdx: number) => {
let indicator = null;
if (cIdx === 0) {
indicator = v;
} else if (v === "") {
indicator = "";
}
if (v === true) {
indicator = (
<Box className={`indicator active`}>
<CircleIcon />
</Box>
);
} else if (v === false) {
indicator = (
<Box className={`indicator deactivated`}>
<CircleIcon />
</Box>
);
}
return (
<td
key={`${rIdx + 1}${cIdx}`}
className={cIdx === 0 ? "feature-cell" : "status-cell"}
>
{indicator}
</td>
);
})}
</tr>
);
});
return (
<LookupTableBase>
<Box sx={{ marginTop: 15, marginBottom: 15 }}>
Replication status for {entityType}: <strong>{entityName}</strong>.
</Box>
<table>
<thead>
<tr>{tableHeader}</tr>
</thead>
<tbody>{tableRowsToRender}</tbody>
</table>
</LookupTableBase>
);
};
export default LookupStatusTable;

View File

@@ -1,85 +0,0 @@
// 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 { StatsResponseType } from "../SiteReplicationStatus";
import LookupStatusTable from "./LookupStatusTable";
import { EntityNotFound, isEntityNotFound, syncStatus } from "./Utils";
type PolicyEntityStatusProps = Partial<StatsResponseType> & {
lookupValue?: string;
};
const PolicyEntityStatus = ({
policyStats = {},
sites = {},
lookupValue = "",
}: PolicyEntityStatusProps) => {
const rowsForStatus = ["Policy"];
const policySites: Record<string, any> = policyStats[lookupValue] || {};
if (!lookupValue) return null;
const siteKeys = Object.keys(sites);
const notFound = isEntityNotFound(sites, policySites, "HasPolicy");
const resultMatrix: any = [];
if (notFound) {
return <EntityNotFound entityType={"Policy"} entityValue={lookupValue} />;
} else {
const row = [];
for (let sCol = 0; sCol < siteKeys.length; sCol++) {
if (sCol === 0) {
row.push("");
}
row.push(sites[siteKeys[sCol]].name);
}
resultMatrix.push(row);
for (let fi = 0; fi < rowsForStatus.length; fi++) {
const sfRow = [];
const feature = rowsForStatus[fi];
let sbStatus: string | boolean = "";
for (let si = 0; si < siteKeys.length; si++) {
const bucketSiteDeploymentId = sites[siteKeys[si]].deploymentID;
const rSite = policySites[bucketSiteDeploymentId];
if (si === 0) {
sfRow.push(feature);
}
switch (fi) {
case 0:
sbStatus = syncStatus(rSite.PolicyMismatch, rSite.HasPolicy);
sfRow.push(sbStatus);
break;
}
}
resultMatrix.push(sfRow);
}
}
return (
<LookupStatusTable
matrixData={resultMatrix}
entityName={lookupValue}
entityType={"Policy"}
/>
);
};
export default PolicyEntityStatus;

View File

@@ -1,91 +0,0 @@
// 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 { StatsResponseType } from "../SiteReplicationStatus";
import LookupStatusTable from "./LookupStatusTable";
import { EntityNotFound, isEntityNotFound, syncStatus } from "./Utils";
type PolicyEntityStatusProps = Partial<StatsResponseType> & {
lookupValue?: string;
};
const UserEntityStatus = ({
userStats = {},
sites = {},
lookupValue = "",
}: PolicyEntityStatusProps) => {
const rowsForStatus = ["Info", "Policy mapping"];
const userSites: Record<string, any> = userStats[lookupValue] || {};
if (!lookupValue) return null;
const siteKeys = Object.keys(sites);
const notFound = isEntityNotFound(sites, userSites, "HasUser");
const resultMatrix: any = [];
if (notFound) {
return <EntityNotFound entityType={"User"} entityValue={lookupValue} />;
} else {
const row = [];
for (let sCol = 0; sCol < siteKeys.length; sCol++) {
if (sCol === 0) {
row.push("");
}
row.push(sites[siteKeys[sCol]].name);
}
resultMatrix.push(row);
for (let fi = 0; fi < rowsForStatus.length; fi++) {
const sfRow = [];
const feature = rowsForStatus[fi];
let sbStatus: string | boolean = "";
for (let si = 0; si < siteKeys.length; si++) {
const bucketSiteDeploymentId = sites[siteKeys[si]].deploymentID;
const rSite = userSites[bucketSiteDeploymentId];
if (si === 0) {
sfRow.push(feature);
}
switch (fi) {
case 0:
sbStatus = syncStatus(rSite.UserInfoMismatch, rSite.HasUser);
sfRow.push(sbStatus);
break;
case 1:
sbStatus = syncStatus(rSite.PolicyMismatch, rSite.HasPolicyMapping);
sfRow.push(sbStatus);
break;
}
}
resultMatrix.push(sfRow);
}
}
return (
<LookupStatusTable
matrixData={resultMatrix}
entityName={lookupValue}
entityType={"User"}
/>
);
};
export default UserEntityStatus;

View File

@@ -1,49 +0,0 @@
import React from "react";
import { StatsResponseType } from "../SiteReplicationStatus";
import { Box } from "mds";
export function syncStatus(mismatch: boolean, set: boolean): string | boolean {
if (!set) {
return "";
}
return !mismatch;
}
export function isEntityNotFound(
sites: Partial<StatsResponseType>,
lookupList: Partial<StatsResponseType>,
lookupKey: string,
) {
const siteKeys: string[] = Object.keys(sites);
return siteKeys.find((sk: string) => {
// there is no way to find the type of this ! as it is an entry in the structure itself.
// @ts-ignore
const result: Record<string, any> = lookupList[sk] || {};
return !result[lookupKey];
});
}
export const EntityNotFound = ({
entityType,
entityValue,
}: {
entityType: string;
entityValue: string;
}) => {
return (
<Box
sx={{
marginTop: "45px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{entityType}:{" "}
<Box sx={{ marginLeft: "5px", marginRight: "5px", fontWeight: 600 }}>
{entityValue}
</Box>{" "}
not found.
</Box>
);
};

View File

@@ -1,147 +0,0 @@
// 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 {
Box,
CircleIcon,
ConfirmDeleteIcon,
DataTable,
IColumns,
ItemActions,
Tooltip,
} from "mds";
import styled from "styled-components";
import get from "lodash/get";
import { ReplicationSite } from "./SiteReplication";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import EditSiteEndPoint from "./EditSiteEndPoint";
const EndpointRender = styled.div(({ theme }) => ({
display: "flex",
gap: 10,
"& .currentIndicator": {
"& .min-icon": {
width: 12,
height: 12,
fill: get(theme, "signalColors.good", "#4CCB92"),
},
},
"& .endpointName": {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
}));
const ReplicationSites = ({
sites,
onDeleteSite,
onRefresh,
}: {
sites: ReplicationSite[];
onDeleteSite: (isAll: boolean, sites: string[]) => void;
onRefresh: () => void;
}) => {
const [deleteSiteKey, setIsDeleteSiteKey] = useState<string>("");
const [editSite, setEditSite] = useState<any>(null);
const replicationColumns: IColumns[] = [
{ label: "Site Name", elementKey: "name" },
{
label: "Endpoint",
elementKey: "endpoint",
renderFullObject: true,
renderFunction: (siteInfo) => (
<EndpointRender>
{siteInfo.isCurrent ? (
<Tooltip tooltip={"This site/cluster"} placement="top">
<Box className={"currentIndicator"}>
<CircleIcon />
</Box>
</Tooltip>
) : null}
<Tooltip tooltip={siteInfo.endpoint}>
<Box className={"endpointName"}>{siteInfo.endpoint}</Box>
</Tooltip>
</EndpointRender>
),
},
];
const actions: ItemActions[] = [
{
type: "edit",
onClick: (valueToSend) => setEditSite(valueToSend),
tooltip: "Edit Endpoint",
},
{
type: "delete",
onClick: (valueToSend) => setIsDeleteSiteKey(valueToSend.name),
tooltip: "Delete Site",
},
];
return (
<Fragment>
<DataTable
columns={replicationColumns}
records={sites}
itemActions={actions}
idField={"name"}
customPaperHeight={"calc(100vh - 660px)"}
sx={{ marginBottom: 20 }}
/>
{deleteSiteKey !== "" && (
<ConfirmDialog
title={`Delete Replication Site`}
confirmText={"Delete"}
isOpen={deleteSiteKey !== ""}
titleIcon={<ConfirmDeleteIcon />}
isLoading={false}
onConfirm={() => {
onDeleteSite(false, [deleteSiteKey]);
}}
onClose={() => {
setIsDeleteSiteKey("");
}}
confirmationContent={
<Fragment>
Are you sure you want to remove the replication site:{" "}
<strong>{deleteSiteKey}</strong>?
</Fragment>
}
/>
)}
{editSite !== null && (
<EditSiteEndPoint
onComplete={() => {
setEditSite(null);
onRefresh();
}}
editSite={editSite}
onClose={() => {
setEditSite(null);
}}
/>
)}
</Fragment>
);
};
export default ReplicationSites;

View File

@@ -1,172 +0,0 @@
// 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 TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import { AddIcon, Box, Button, Grid, InputBox, RemoveIcon } from "mds";
import { SiteInputRow } from "./Types";
interface ISRSiteInputRowProps {
rowData: SiteInputRow;
rowId: number;
onFieldChange: (e: any, fieldName: string, index: number) => void;
onAddClick?: (index: number) => void;
onRemoveClick?: (index: number) => void;
canAdd?: boolean;
canRemove?: boolean;
showRowActions?: boolean;
disabledFields?: string[];
fieldErrors?: Record<string, string>;
}
const SRSiteInputRow = ({
rowData,
rowId: index,
onFieldChange,
onAddClick,
onRemoveClick,
canAdd = true,
canRemove = true,
showRowActions = true,
disabledFields = [],
fieldErrors = {},
}: ISRSiteInputRowProps) => {
const { endpoint = "", accessKey = "", secretKey = "", name = "" } = rowData;
return (
<Fragment key={`${index}`}>
<Box>
<InputBox
id={`add-rep-peer-site-${index}`}
name={`add-rep-peer-site-${index}`}
placeholder={`site-name`}
label=""
readOnly={disabledFields.includes("name")}
value={name}
onChange={(e) => {
onFieldChange(e, "name", index);
}}
data-test-id={`add-site-rep-peer-site-${index}`}
/>
</Box>
<Box>
<InputBox
id={`add-rep-peer-site-ep-${index}`}
name={`add-rep-peer-site-ep-${index}`}
placeholder={`https://dr.minio-storage:900${index}`}
label=""
readOnly={disabledFields.includes("endpoint")}
error={fieldErrors["endpoint"]}
value={endpoint}
onChange={(e) => {
onFieldChange(e, "endpoint", index);
}}
data-test-id={`add-site-rep-peer-ep-${index}`}
/>
</Box>
<Box>
<InputBox
id={`add-rep-peer-site-ac-${index}`}
name={`add-rep-peer-site-ac-${index}`}
label=""
required={true}
disabled={disabledFields.includes("accessKey")}
value={accessKey}
error={fieldErrors["accessKey"]}
onChange={(e) => {
onFieldChange(e, "accessKey", index);
}}
data-test-id={`add-rep-peer-site-ac-${index}`}
/>
</Box>
<Box>
<InputBox
id={`add-rep-peer-site-sk-${index}`}
name={`add-rep-peer-site-sk-${index}`}
label=""
required={true}
type={"password"}
value={secretKey}
error={fieldErrors["secretKey"]}
disabled={disabledFields.includes("secretKey")}
onChange={(e) => {
onFieldChange(e, "secretKey", index);
}}
data-test-id={`add-rep-peer-site-sk-${index}`}
/>
</Box>
<Grid item xs={12} sx={{ alignItems: "center", display: "flex" }}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
alignSelf: "baseline",
marginTop: "4px",
"& button": {
borderColor: "#696969",
color: "#696969",
borderRadius: "50%",
},
}}
>
{showRowActions ? (
<React.Fragment>
<TooltipWrapper tooltip={"Add a Row"}>
<Button
id={`add-row-${index}`}
variant="regular"
disabled={!canAdd}
icon={<AddIcon />}
onClick={(e) => {
e.preventDefault();
onAddClick?.(index);
}}
style={{
width: 25,
height: 25,
padding: 0,
}}
/>
</TooltipWrapper>
<TooltipWrapper tooltip={"Remove Row"}>
<Button
id={`remove-row-${index}`}
variant="regular"
disabled={!canRemove}
icon={<RemoveIcon />}
onClick={(e) => {
e.preventDefault();
onRemoveClick?.(index);
}}
style={{
width: 25,
height: 25,
padding: 0,
marginLeft: 8,
}}
/>
</TooltipWrapper>
</React.Fragment>
) : null}
</Box>
</Grid>
</Fragment>
);
};
export default SRSiteInputRow;

View File

@@ -1,311 +0,0 @@
// 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 { useNavigate } from "react-router-dom";
import {
ActionLink,
AddIcon,
Box,
Button,
ClustersIcon,
ConfirmDeleteIcon,
Grid,
HelpBox,
Loader,
PageLayout,
RecoverIcon,
SectionTitle,
TrashIcon,
} from "mds";
import { ErrorResponseHandler } from "../../../../common/types";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
import {
setErrorSnackMessage,
setHelpName,
setSnackBarMessage,
} from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import useApi from "../../Common/Hooks/useApi";
import ReplicationSites from "./ReplicationSites";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../../HelpMenu";
export type ReplicationSite = {
deploymentID: string;
endpoint: string;
name: string;
isCurrent?: boolean;
};
const SiteReplication = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [sites, setSites] = useState([]);
const [deleteAll, setIsDeleteAll] = useState(false);
const [isSiteInfoLoading, invokeSiteInfoApi] = useApi(
(res: any) => {
const { sites: siteList, name: curSiteName } = res;
// current site name to be the fist one.
const foundIdx = siteList.findIndex((el: any) => el.name === curSiteName);
if (foundIdx !== -1) {
let curSite = siteList[foundIdx];
curSite = {
...curSite,
isCurrent: true,
};
siteList.splice(foundIdx, 1, curSite);
}
siteList.sort((x: any, y: any) => {
return x.name === curSiteName ? -1 : y.name === curSiteName ? 1 : 0;
});
setSites(siteList);
},
(err: any) => {
setSites([]);
},
);
const getSites = () => {
invokeSiteInfoApi("GET", `api/v1/admin/site-replication`);
};
const [isRemoving, invokeSiteRemoveApi] = useApi(
(res: any) => {
setIsDeleteAll(false);
dispatch(setSnackBarMessage(`Successfully deleted.`));
getSites();
},
(err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
},
);
const removeSites = (isAll: boolean = false, delSites: string[] = []) => {
invokeSiteRemoveApi("DELETE", `api/v1/admin/site-replication`, {
all: isAll,
sites: delSites,
});
};
useEffect(() => {
getSites();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const hasSites = sites?.length;
useEffect(() => {
dispatch(setHelpName("site-replication"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Fragment>
<PageHeaderWrapper label={"Site Replication"} actions={<HelpMenu />} />
<PageLayout>
<SectionTitle
separator={!!hasSites}
sx={{ marginBottom: 15 }}
actions={
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
gap: 8,
}}
>
{hasSites ? (
<Fragment>
<TooltipWrapper tooltip={"Delete All"}>
<Button
id={"delete-all"}
label={"Delete All"}
variant="secondary"
disabled={isRemoving}
icon={<TrashIcon />}
onClick={() => {
setIsDeleteAll(true);
}}
/>
</TooltipWrapper>
<TooltipWrapper tooltip={"Replication Status"}>
<Button
id={"replication-status"}
label={"Replication Status"}
variant="regular"
icon={<RecoverIcon />}
onClick={(e) => {
e.preventDefault();
navigate(IAM_PAGES.SITE_REPLICATION_STATUS);
}}
/>
</TooltipWrapper>
</Fragment>
) : null}
<TooltipWrapper tooltip={"Add Replication Sites"}>
<Button
id={"add-replication-site"}
label={"Add Sites"}
variant="callAction"
disabled={isRemoving}
icon={<AddIcon />}
onClick={() => {
navigate(IAM_PAGES.SITE_REPLICATION_ADD);
}}
/>
</TooltipWrapper>
</Box>
}
>
{hasSites ? "List of Replicated Sites" : ""}
</SectionTitle>
{hasSites ? (
<ReplicationSites
sites={sites}
onDeleteSite={removeSites}
onRefresh={getSites}
/>
) : null}
{isSiteInfoLoading ? (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "calc( 100vh - 450px )",
}}
>
<Loader style={{ width: 16, height: 16 }} />
</Box>
) : null}
{!hasSites && !isSiteInfoLoading ? (
<Grid container>
<Grid item xs={8}>
<HelpBox
title={"Site Replication"}
iconComponent={<ClustersIcon />}
help={
<Fragment>
This feature allows multiple independent MinIO sites (or
clusters) that are using the same external IDentity Provider
(IDP) to be configured as replicas.
<br />
<br />
To get started,{" "}
<ActionLink
isLoading={false}
label={""}
onClick={() => {
navigate(IAM_PAGES.SITE_REPLICATION_ADD);
}}
>
Add a Replication Site
</ActionLink>
.
<br />
You can learn more at our{" "}
<a
href="https://min.io/docs/minio/linux/operations/install-deploy-manage/multi-site-replication.html?ref=con"
target="_blank"
rel="noopener"
>
documentation
</a>
.
</Fragment>
}
/>
</Grid>
</Grid>
) : null}
{hasSites && !isSiteInfoLoading ? (
<HelpBox
title={"Site Replication"}
iconComponent={<ClustersIcon />}
help={
<Fragment>
This feature allows multiple independent MinIO sites (or
clusters) that are using the same external IDentity Provider
(IDP) to be configured as replicas. In this situation the set of
replica sites are referred to as peer sites or just sites.
<br />
<br />
Initially, only one of the sites added for replication may have
data. After site-replication is successfully configured, this
data is replicated to the other (initially empty) sites.
Subsequently, objects may be written to any of the sites, and
they will be replicated to all other sites.
<br />
<br />
All sites must have the same deployment credentials (i.e.
MINIO_ROOT_USER, MINIO_ROOT_PASSWORD).
<br />
<br />
All sites must be using the same external IDP(s) if any.
<br />
<br />
For SSE-S3 or SSE-KMS encryption via KMS, all sites must have
access to a central KMS deployment server.
<br />
<br />
You can learn more at our{" "}
<a
href="https://github.com/minio/minio/tree/master/docs/site-replication?ref=con"
target="_blank"
rel="noopener"
>
documentation
</a>
.
</Fragment>
}
/>
) : null}
{deleteAll ? (
<ConfirmDialog
title={`Delete All`}
confirmText={"Delete"}
isOpen={true}
titleIcon={<ConfirmDeleteIcon />}
isLoading={false}
onConfirm={() => {
const siteNames = sites.map((s: any) => s.name);
removeSites(true, siteNames);
}}
onClose={() => {
setIsDeleteAll(false);
}}
confirmationContent={
<Fragment>
Are you sure you want to remove all the replication sites?.
</Fragment>
}
/>
) : null}
</PageLayout>
</Fragment>
);
};
export default SiteReplication;

View File

@@ -1,251 +0,0 @@
// 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 {
BackLink,
Box,
breakPoints,
BucketsIcon,
Button,
Grid,
GroupsIcon,
IAMPoliciesIcon,
Loader,
PageLayout,
RefreshIcon,
UsersIcon,
SectionTitle,
} from "mds";
import { useNavigate } from "react-router-dom";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
import { useAppDispatch } from "../../../../store";
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
import StatusCountCard from "../../Dashboard/BasicDashboard/StatusCountCard";
import EntityReplicationLookup from "./EntityReplicationLookup";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../../HelpMenu";
import { api } from "api";
import { errorToHandler } from "api/errors";
import {
ApiError,
HttpResponse,
SiteReplicationStatusResponse,
} from "api/consoleApi";
export type StatsResponseType = {
maxBuckets?: number;
bucketStats?: Record<string, any>;
maxGroups?: number;
groupStats?: Record<string, any>;
maxUsers?: number;
userStats?: Record<string, any>;
maxPolicies?: number;
policyStats?: Record<string, any>;
sites?: Record<string, any>;
};
const SREntityStatus = ({
maxValue = 0,
entityStatObj = {},
entityTextPlural = "",
icon = null,
}: {
maxValue: number;
entityStatObj: Record<string, any>;
entityTextPlural: string;
icon?: React.ReactNode;
}) => {
const statEntityLen = Object.keys(entityStatObj || {})?.length;
return (
<Box
withBorders
sx={{
padding: "25px",
[`@media (min-width: ${breakPoints.sm}px)`]: {
maxWidth: "100%",
},
}}
>
<StatusCountCard
icon={icon}
onlineCount={maxValue}
offlineCount={statEntityLen}
okStatusText={"Synced"}
notOkStatusText={"Failed"}
label={entityTextPlural}
/>
</Box>
);
};
const SiteReplicationStatus = () => {
const navigate = useNavigate();
const [stats, setStats] = useState<StatsResponseType>({});
const [loading, setLoading] = useState<boolean>(false);
const {
maxBuckets = 0,
bucketStats = {},
maxGroups = 0,
groupStats = {},
maxUsers = 0,
userStats = {},
maxPolicies = 0,
policyStats = {},
} = stats || {};
const getStats = () => {
setLoading(true);
api.admin
.getSiteReplicationStatus({
buckets: true,
groups: true,
policies: true,
users: true,
})
.then((res: HttpResponse<SiteReplicationStatusResponse, ApiError>) => {
setStats(res.data);
})
.catch((res: HttpResponse<SiteReplicationStatusResponse, ApiError>) => {
setStats({});
dispatch(setErrorSnackMessage(errorToHandler(res.error)));
})
.finally(() => setLoading(false));
};
useEffect(() => {
getStats();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setHelpName("replication_status"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Fragment>
<PageHeaderWrapper
label={
<BackLink
label={"Site Replication"}
onClick={() => navigate(IAM_PAGES.SITE_REPLICATION)}
/>
}
actions={<HelpMenu />}
/>
<PageLayout>
<SectionTitle
actions={
<Fragment>
<TooltipWrapper tooltip={"Refresh"}>
<Button
id={"refresh"}
onClick={() => {
getStats();
}}
label={"Refresh"}
icon={<RefreshIcon />}
variant={"regular"}
collapseOnSmall={false}
/>
</TooltipWrapper>
</Fragment>
}
separator
>
Replication status from all Sites
</SectionTitle>
{!loading ? (
<Box
sx={{
display: "grid",
marginTop: "25px",
gridTemplateColumns: "1fr 1fr 1fr 1fr",
[`@media (max-width: ${breakPoints.md}px)`]: {
gridTemplateColumns: "1fr 1fr",
},
[`@media (max-width: ${breakPoints.sm}px)`]: {
gridTemplateColumns: "1fr",
},
gap: "30px",
}}
>
<SREntityStatus
entityStatObj={bucketStats}
entityTextPlural={"Buckets"}
maxValue={maxBuckets}
icon={<BucketsIcon />}
/>
<SREntityStatus
entityStatObj={userStats}
entityTextPlural={"Users"}
maxValue={maxUsers}
icon={<UsersIcon />}
/>
<SREntityStatus
entityStatObj={groupStats}
entityTextPlural={"Groups"}
maxValue={maxGroups}
icon={<GroupsIcon />}
/>
<SREntityStatus
entityStatObj={policyStats}
entityTextPlural={"Policies"}
maxValue={maxPolicies}
icon={<IAMPoliciesIcon />}
/>
</Box>
) : (
<Grid
item
xs={12}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
marginTop: 45,
}}
>
<Loader style={{ width: 25, height: 25 }} />
</Grid>
)}
<Box
withBorders
sx={{
minHeight: 450,
[`@media (max-width: ${breakPoints.sm}px)`]: {
minHeight: 250,
},
marginTop: "25px",
padding: "25px",
}}
>
<EntityReplicationLookup />
</Box>
</PageLayout>
</Fragment>
);
};
export default SiteReplicationStatus;

View File

@@ -1,8 +0,0 @@
export type SiteInputRow = {
name: string;
endpoint: string;
accessKey: string;
secretKey: string;
isCurrent?: boolean;
isSaved?: boolean;
};

View File

@@ -123,16 +123,6 @@ const ConfigurationOptions = React.lazy(
);
const AddGroupScreen = React.lazy(() => import("./Groups/AddGroupScreen"));
const SiteReplication = React.lazy(
() => import("./Configurations/SiteReplication/SiteReplication"),
);
const SiteReplicationStatus = React.lazy(
() => import("./Configurations/SiteReplication/SiteReplicationStatus"),
);
const AddReplicationSites = React.lazy(
() => import("./Configurations/SiteReplication/AddReplicationSites"),
);
const KMSRoutes = React.lazy(() => import("./KMS/KMSRoutes"));
@@ -358,18 +348,6 @@ const Console = () => {
component: ListTiersConfiguration,
path: IAM_PAGES.TIERS,
},
{
component: SiteReplication,
path: IAM_PAGES.SITE_REPLICATION,
},
{
component: SiteReplicationStatus,
path: IAM_PAGES.SITE_REPLICATION_STATUS,
},
{
component: AddReplicationSites,
path: IAM_PAGES.SITE_REPLICATION_ADD,
},
{
component: Account,
path: IAM_PAGES.ACCOUNT,

View File

@@ -40,7 +40,6 @@ import {
MetricsMenuIcon,
MonitoringMenuIcon,
ObjectBrowserIcon,
RecoverIcon,
SettingsIcon,
TiersIcon,
UsersMenuIcon,
@@ -219,13 +218,6 @@ export const validRoutes = (
icon: <TiersIcon />,
id: "tiers",
},
{
group: "Administrator",
path: IAM_PAGES.SITE_REPLICATION,
name: "Site Replication",
icon: <RecoverIcon />,
id: "sitereplication",
},
{
group: "Administrator",
path: IAM_PAGES.KMS_KEYS,

View File

@@ -197,7 +197,6 @@ export const {
setModalErrorSnackMessage,
setModalSnackMessage,
globalSetDistributedSetup,
setSiteReplicationInfo,
setOverrideStyles,
setAnonymousMode,
resetSystem,

View File

@@ -1,49 +0,0 @@
// 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 roles from "../utils/roles";
import { IAM_PAGES } from "../../src/common/SecureComponent/permissions";
import { Selector } from "testcafe";
import { getMenuElement } from "../utils/elements-menu";
let testDomainUrl = "http://localhost:9090";
const screenUrl = `${testDomainUrl}${IAM_PAGES.SITE_REPLICATION}`;
const siteReplicationEl = getMenuElement("sitereplication");
export const addSitesBtn = Selector("button").withText("Add Sites");
/* Begin Local Testing config block */
// For local Testing Create users and assign policies then update here.
// Command to invoke the test locally: testcafe chrome tests/permissions/site-replication.ts
/* End Local Testing config block */
fixture("Site Replication Status for user with Admin permissions")
.page(testDomainUrl)
.beforeEach(async (t) => {
await t.useRole(roles.settings);
});
test("Site replication sidebar item exists", async (t) => {
await t.expect(siteReplicationEl.exists).ok();
});
test("Add Sites button exists", async (t) => {
const addSitesBtnExists = addSitesBtn.exists;
await t.navigateTo(screenUrl).expect(addSitesBtnExists).ok();
});
test("Add Sites button is clickable", async (t) => {
await t.navigateTo(screenUrl).click(addSitesBtn);
});

View File

@@ -1,63 +0,0 @@
// 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 roles from "../utils/roles";
import * as elements from "../utils/elements";
import { bucketDropdownOptionFor } from "../utils/elements";
import * as functions from "../utils/functions";
import { monitoringElement, watchElement } from "../utils/elements-menu";
fixture("For user with Watch permissions")
.page("http://localhost:9090")
.beforeEach(async (t) => {
await t.useRole(roles.watch);
});
test("Monitoring sidebar item exists", async (t) => {
await t.expect(monitoringElement.exists).ok();
});
test("Watch link exists in Support page", async (t) => {
await t
.expect(monitoringElement.exists)
.ok()
.click(monitoringElement)
.expect(watchElement.exists)
.ok();
});
test("Watch page can be opened", async (t) => {
await t.navigateTo("http://localhost:9090/tools/watch");
});
test
.before(async (t) => {
// Create a bucket
await functions.setUpBucket(t, "watch");
})("Start button can be clicked", async (t) => {
await t
// We need to log back in after we use the admin account to create bucket,
// using the specific role we use in this module
.useRole(roles.watch)
.navigateTo("http://localhost:9090/tools/watch")
.click(elements.bucketNameInput)
.click(bucketDropdownOptionFor("watch"))
.click(elements.startButton);
})
.after(async (t) => {
// Cleanup created bucket
await functions.cleanUpBucket(t, "watch");
});

View File

@@ -1,40 +0,0 @@
// 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 roles from "../utils/roles";
import * as elements from "../utils/elements";
import { diagnosticsElement } from "../utils/elements-menu";
fixture("For user with Diagnostics permissions").page("http://localhost:9090");
test("Diagnostics link exists in Tools page", async (t) => {
await t.useRole(roles.diagnostics).expect(diagnosticsElement.exists).ok();
});
test("Diagnostics page can be opened", async (t) => {
await t
.useRole(roles.diagnostics)
.navigateTo("http://localhost:9090/support/diagnostics");
});
test("Start Diagnostic button exists", async (t) => {
const startDiagnosticExists = elements.startDiagnosticButton.exists;
await t
.useRole(roles.diagnostics)
.navigateTo("http://localhost:9090/support/diagnostics")
.expect(startDiagnosticExists)
.ok();
});

View File

@@ -100,7 +100,7 @@ __init__() {
main() {
(yarn start &>/dev/null) &
(./console server &>/dev/null) &
(testcafe "chrome:headless" "$1" -q --skip-js-errors -c 3)
(testcafe "firefox:headless" "$1" -q --skip-js-errors -c 3)
cleanup
}

View File

@@ -14947,16 +14947,6 @@ __metadata:
languageName: node
linkType: hard
"react-use-websocket@npm:^4.8.1":
version: 4.8.1
resolution: "react-use-websocket@npm:4.8.1"
peerDependencies:
react: ">= 18.0.0"
react-dom: ">= 18.0.0"
checksum: 10c0/9106947334badc06d8af635328f1420068098130eac8f6da90e7d13cf37037cf1cf0bd528103ca3689a38097a70e10d3b577f143078254e9dd3b9aa429395116
languageName: node
linkType: hard
"react-virtual@npm:^2.8.2":
version: 2.10.4
resolution: "react-virtual@npm:2.10.4"
@@ -18194,7 +18184,6 @@ __metadata:
react-redux: "npm:^8.1.3"
react-router-dom: "npm:6.25.1"
react-scripts: "npm:5.0.1"
react-use-websocket: "npm:^4.8.1"
react-virtualized: "npm:^9.22.5"
react-window: "npm:^1.8.10"
react-window-infinite-loader: "npm:^1.0.9"