Single screen to display and edit Prometheus monitoring configuration (#2134)

* Created new screen to display and edit Prometheus monitoring configuration
* Updated image name validation to include slash and colon
* Removed unused files
This commit is contained in:
jinapurapu
2022-06-21 09:39:32 -07:00
committed by GitHub
parent 41f640077b
commit d0b65ce297
7 changed files with 625 additions and 782 deletions

View File

@@ -1,428 +0,0 @@
import React, { useEffect, useState } from "react";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Theme } from "@mui/material/styles";
import {
formFieldStyles,
modalBasic,
modalStyleUtils,
} from "../../Common/FormComponents/common/styleLibrary";
import { Button, Grid } from "@mui/material";
import api from "../../../../common/api";
import { IKeyValue, ITenant } from "../ListTenants/types";
import { ErrorResponseHandler } from "../../../../common/types";
import KeyPairEdit from "./KeyPairEdit";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import {
commonFormValidation,
IValidation,
} from "../../../../utils/validationFunctions";
import InputUnitMenu from "../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
import { setModalErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
interface IEditTenantMonitoringProps {
tenant: ITenant;
classes: any;
open: boolean;
onClose: (shouldReload: boolean) => void;
image: string;
sidecarImage: string;
initImage: string;
labels: IKeyValue[];
annotations: IKeyValue[];
nodeSelector: IKeyValue[];
diskCapacityGB: number;
serviceAccountName: string;
tenantName: string;
tenantNamespace: string;
storageClassName: string;
cpuRequest: string;
memRequest: string;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
...modalBasic,
...modalStyleUtils,
...formFieldStyles,
});
const EditTenantMonitoringModal = ({
tenant,
classes,
open,
onClose,
image,
sidecarImage,
initImage,
labels,
annotations,
nodeSelector,
diskCapacityGB,
serviceAccountName,
storageClassName,
tenantName,
tenantNamespace,
cpuRequest,
memRequest,
}: IEditTenantMonitoringProps) => {
const dispatch = useAppDispatch();
const [validationErrors, setValidationErrors] = useState<any>({});
const [newLabels, setNewLabels] = useState<IKeyValue[]>(
labels.length > 0 ? [...labels] : [{ key: "", value: "" }]
);
const [newAnnotations, setNewAnnotations] = useState<IKeyValue[]>(
annotations.length > 0 ? [...annotations] : [{ key: "", value: "" }]
);
const [newNodeSelector, setNewNodeSelector] = useState<IKeyValue[]>(
nodeSelector.length > 0 ? [...nodeSelector] : [{ key: "", value: "" }]
);
const [newImage, setNewImage] = useState<string>(image);
const [newSidecarImage, setNewSidecarImage] = useState<string>(sidecarImage);
const [newInitImage, setNewInitImage] = useState<string>(initImage);
const [newDiskCapacityGB, setNewDiskCapacityGB] = useState<string>(
diskCapacityGB.toString()
);
const [newCPURequest, setNewCPURequest] = useState<string>(cpuRequest);
const [newMemRequest, setNewMemRequest] = useState<string>(
memRequest
? Math.floor(parseInt(memRequest, 10) / 1000000000).toString()
: ""
);
const [newServiceAccountName, setNewServiceAccountName] =
useState<string>(serviceAccountName);
const [newStorageClassName, setNewStorageClassName] =
useState<string>(storageClassName);
const [labelsError, setLabelsError] = useState<any>({});
const [annotationsError, setAnnotationsError] = useState<any>({});
const [nodeSelectorError, setNodeSelectorError] = useState<any>({});
const trim = (x: IKeyValue[]): IKeyValue[] => {
let retval: IKeyValue[] = [];
for (let i = 0; i < x.length; i++) {
if (x[i].key !== "") {
retval.push(x[i]);
}
}
return retval;
};
useEffect(() => {
let tenantMonitoringValidation: IValidation[] = [];
tenantMonitoringValidation.push({
fieldKey: `image`,
required: false,
value: newImage,
pattern:
/^([a-zA-Z0-9])([a-zA-Z0-9-._])*([a-zA-Z0-9]?)+(\/(([a-zA-Z0-9])([a-zA-Z0-9-._])*([a-zA-Z0-9])?)+)*:([a-zA-Z0-9])[a-zA-Z0-9-.]{0,127}$/,
customPatternMessage: "Invalid image",
});
tenantMonitoringValidation.push({
fieldKey: `sidecarImage`,
required: false,
value: newSidecarImage,
pattern:
/^([a-zA-Z0-9])([a-zA-Z0-9-._])*([a-zA-Z0-9]?)+(\/(([a-zA-Z0-9])([a-zA-Z0-9-._])*([a-zA-Z0-9])?)+)*:([a-zA-Z0-9])[a-zA-Z0-9-.]{0,127}$/,
customPatternMessage: "Invalid image",
});
tenantMonitoringValidation.push({
fieldKey: `initImage`,
required: false,
value: newInitImage,
pattern:
/^([a-zA-Z0-9])([a-zA-Z0-9-._])*([a-zA-Z0-9]?)+(\/(([a-zA-Z0-9])([a-zA-Z0-9-._])*([a-zA-Z0-9])?)+)*:([a-zA-Z0-9])[a-zA-Z0-9-.]{0,127}$/,
customPatternMessage: "Invalid image",
});
tenantMonitoringValidation.push({
fieldKey: `diskCapacityGB`,
required: true,
value: newDiskCapacityGB as any as string,
pattern: /^[0-9]*$/,
customPatternMessage: "Must be an integer between 0 and 10",
});
tenantMonitoringValidation.push({
fieldKey: `newCPURequest`,
required: false,
value: newCPURequest as any as string,
pattern: /^[0-9]*$/,
customPatternMessage: "Must be an integer between 0 and 10",
});
tenantMonitoringValidation.push({
fieldKey: `newMemRequest`,
required: false,
value: newMemRequest as any as string,
pattern: /^[0-9]*$/,
customPatternMessage: "Must be an integer between 0 and 10",
});
tenantMonitoringValidation.push({
fieldKey: `serviceAccountName`,
required: false,
value: newServiceAccountName,
pattern: /^[a-zA-Z0-9-.]{1,253}$/,
customPatternMessage: "Invalid service account name",
});
tenantMonitoringValidation.push({
fieldKey: `storageClassName`,
required: false,
value: newStorageClassName,
pattern: /^[a-zA-Z0-9-.]{1,253}$/,
customPatternMessage: "Invalid storage class name",
});
const commonVal = commonFormValidation(tenantMonitoringValidation);
setValidationErrors(commonVal);
}, [
newImage,
newSidecarImage,
newInitImage,
newDiskCapacityGB,
newServiceAccountName,
newStorageClassName,
newCPURequest,
newMemRequest,
setValidationErrors,
]);
const checkValid = (): boolean => {
if (
Object.keys(validationErrors).length !== 0 ||
Object.keys(labelsError).length !== 0 ||
Object.keys(annotationsError).length !== 0 ||
Object.keys(nodeSelectorError).length !== 0
) {
let err: ErrorResponseHandler = {
errorMessage: "Invalid entry",
detailedError: "",
};
dispatch(setModalErrorSnackMessage(err));
return false;
} else {
return true;
}
};
const submitMonitoringInfo = (event: React.FormEvent) => {
event.preventDefault();
api
.invoke(
"PUT",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/monitoring`,
{
labels: trim(newLabels),
annotations: trim(newAnnotations),
nodeSelector: trim(newNodeSelector),
image: newImage,
sidecarImage: newSidecarImage,
initImage: newInitImage,
diskCapacityGB: newDiskCapacityGB,
serviceAccountName: newServiceAccountName,
storageClassName: newStorageClassName,
monitoringCPURequest: newCPURequest,
monitoringMemRequest: newMemRequest + "Gi",
}
)
.then(() => {
onClose(true);
})
.catch((err: ErrorResponseHandler) => {});
};
return (
<ModalWrapper
onClose={() => onClose(true)}
modalOpen={open}
title="Edit Monitoring Configuration"
>
<form noValidate autoComplete="off" onSubmit={submitMonitoringInfo}>
<Grid container>
<Grid xs={12} className={classes.modalFormScrollable}>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id={`image`}
label={"Image"}
placeholder={"quay.io/prometheus/prometheus:latest"}
name={`image`}
value={newImage}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setNewImage(event.target.value);
}}
key={`image`}
error={validationErrors[`image`] || ""}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id={`sidecarImage`}
label={"Sidecar Image"}
placeholder={"library/alpine:latest"}
name={`sidecarImage`}
value={newSidecarImage}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setNewSidecarImage(event.target.value);
}}
key={`sidecarImage`}
error={validationErrors[`sidecarImage`] || ""}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id={`initImage`}
label={"Init Image"}
placeholder={"library/busybox:1.33.1"}
name={`initImage`}
value={newInitImage}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setNewInitImage(event.target.value);
}}
key={`initImage`}
error={validationErrors[`initImage`] || ""}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id={`diskCapacityGB`}
label={"Disk Capacity"}
placeholder={"Disk Capacity"}
name={`diskCapacityGB`}
value={newDiskCapacityGB}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setNewDiskCapacityGB(event.target.value);
}}
key={`diskCapacityGB`}
error={validationErrors[`diskCapacityGB`] || ""}
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id={`cpuRequest`}
label={"CPU Request"}
placeholder={"CPU Request"}
name={`cpuRequest`}
value={newCPURequest}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setNewCPURequest(event.target.value);
}}
key={`cpuRequest`}
error={validationErrors[`cpuRequest`] || ""}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id={`memRequest`}
label={"Memory Request"}
placeholder={"Memory request"}
name={`memRequest`}
value={newMemRequest}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
setNewMemRequest(event.target.value);
}
}}
pattern={"[0-9]*"}
key={`memRequest`}
error={validationErrors[`memRequest`] || ""}
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id={`serviceAccountName`}
label={"Service Account"}
placeholder={"Service Account Name"}
name={`serviceAccountName`}
value={newServiceAccountName}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setNewServiceAccountName(event.target.value);
}}
key={`serviceAccountName`}
error={validationErrors[`serviceAccountName`] || ""}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id={`storageClassName`}
label={"Storage Class"}
placeholder={"Storage Class Name"}
name={`storageClassName`}
value={newStorageClassName}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setNewStorageClassName(event.target.value);
}}
key={`storageClassName`}
error={validationErrors[`storageClassName`] || ""}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<span className={classes.inputLabel}>Labels</span>
<KeyPairEdit
newValues={newLabels}
setNewValues={setNewLabels}
paramName={"Labels"}
error={labelsError}
setError={setLabelsError}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<span className={classes.inputLabel}>Annotations</span>
<KeyPairEdit
newValues={newAnnotations}
setNewValues={setNewAnnotations}
paramName={"Annotations"}
error={annotationsError}
setError={setAnnotationsError}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<span className={classes.inputLabel}>Node Selector</span>
<KeyPairEdit
newValues={newNodeSelector}
setNewValues={setNewNodeSelector}
paramName={"Node Selector"}
error={nodeSelectorError}
setError={setNodeSelectorError}
/>
</Grid>
</Grid>
<Grid xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={!checkValid()}
>
Save
</Button>
</Grid>
</Grid>
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(EditTenantMonitoringModal);

View File

@@ -0,0 +1,507 @@
// 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 { ISecurityContext} from "../types";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
containerForHeader,
createTenantCommon,
formFieldStyles,
modalBasic,
spacingUtils,
tenantDetailsStyles,
wizardCommon,
} from "../../Common/FormComponents/common/styleLibrary";
import React, { Fragment, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { AppState, useAppDispatch } from "../../../../store";
import api from "../../../../common/api";
import { ErrorResponseHandler } from "../../../../common/types";
import { useParams } from "react-router-dom";
import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import Grid from "@mui/material/Grid";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { Button, DialogContentText } from "@mui/material";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import { setErrorSnackMessage, setSnackBarMessage } from "../../../../systemSlice";
import { IKeyValue } from "../ListTenants/types";
import KeyPairEdit from "./KeyPairEdit";
import InputUnitMenu from "../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
import { ITenantMonitoringStruct } from "../ListTenants/types";
import {setPrometheusEnabled,
setImage,
setSidecarImage,
setInitImage,
setStorageClassName,
setDiskCapacityGB,
setServiceAccountName,
setCPURequest,
setMemRequest,
} from "../TenantDetails/tenantMonitoringSlice"
import { clearValidationError } from "../utils";
interface ITenantMonitoring {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
...tenantDetailsStyles,
...spacingUtils,
bold: { fontWeight: "bold" },
italic: { fontStyle: "italic" },
fileItem: {
marginRight: 10,
display: "flex",
"& div label": {
minWidth: 50,
},
"@media (max-width: 900px)": {
flexFlow: "column",
},
},
...containerForHeader(theme.spacing(4)),
...createTenantCommon,
...formFieldStyles,
...modalBasic,
...wizardCommon,
});
const TenantMonitoring = ({ classes }: ITenantMonitoring) => {
const dispatch = useAppDispatch();
const { tenantName, tenantNamespace } = useParams();
const prometheusEnabled = useSelector((state: AppState) => state.editTenantMonitoring.prometheusEnabled)
const image = useSelector((state: AppState) => state.editTenantMonitoring.image)
const sidecarImage = useSelector((state: AppState) => state.editTenantMonitoring.sidecarImage)
const initImage = useSelector((state: AppState) => state.editTenantMonitoring.initImage)
const diskCapacityGB = useSelector((state: AppState) => state.editTenantMonitoring.diskCapacityGB)
const cpuRequest = useSelector((state: AppState) => state.editTenantMonitoring.monitoringCPURequest)
const memRequest = useSelector((state: AppState) => state.editTenantMonitoring.monitoringMemRequest)
const serviceAccountName = useSelector((state: AppState) => state.editTenantMonitoring.serviceAccountName)
const storageClassName = useSelector((state: AppState) => state.editTenantMonitoring.storageClassName)
const [validationErrors, setValidationErrors] = useState<any>({});
const [toggleConfirmOpen, setToggleConfirmOpen] = useState<boolean>(false);
const [labels, setLabels] = useState<IKeyValue[]>([{ key: "", value: "" }] );
const [annotations, setAnnotations] = useState<IKeyValue[]>( [{ key: "", value: "" }] );
const [nodeSelector, setNodeSelector] = useState<IKeyValue[]>([{ key: "", value: "" }] );
const [refreshMonitoringInfo, setRefreshMonitoringInfo] =
useState<boolean>(true);
const [labelsError, setLabelsError] = useState<any>({});
const [annotationsError, setAnnotationsError] = useState<any>({});
const [nodeSelectorError, setNodeSelectorError] = useState<any>({});
const cleanValidation = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
const setMonitoringInfo = (res : ITenantMonitoringStruct) => {
dispatch(setImage(res.image));
dispatch(setSidecarImage(res.sidecarImage));
dispatch(setInitImage(res.initImage));
dispatch(setStorageClassName(res.storageClassName));
dispatch(setDiskCapacityGB(res.diskCapacityGB));
dispatch(setServiceAccountName(res.serviceAccountName));
dispatch(setCPURequest(res.monitoringCPURequest));
if (res.monitoringMemRequest) {
dispatch(setMemRequest(Math.floor(parseInt(res.monitoringMemRequest, 10) / 1000000000).toString()));
} else {
dispatch(setMemRequest("0"));
}
res.labels != null ? setLabels(res.labels) : setLabels([{key:"", value:""}]);
res.annotations != null ? setAnnotations(res.annotations) :setAnnotations([{key:"", value:""}]);
res.nodeSelector != null ? setNodeSelector(res.nodeSelector) :setNodeSelector([{key:"", value:""}]);
}
const trim = (x: IKeyValue[]): IKeyValue[] => {
let retval: IKeyValue[] = [];
for (let i = 0; i < x.length; i++) {
if (x[i].key !== "") {
retval.push(x[i]);
}
}
return retval;
};
const checkValid = (): boolean => {
if (
Object.keys(validationErrors).length !== 0 ||
Object.keys(labelsError).length !== 0 ||
Object.keys(annotationsError).length !== 0 ||
Object.keys(nodeSelectorError).length !== 0
) {
let err: ErrorResponseHandler = {
errorMessage: "Invalid entry",
detailedError: "",
};
dispatch(setErrorSnackMessage(err));
return false;
} else {
return true;
}
};
useEffect(() => {
if (refreshMonitoringInfo) {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace || ""}/tenants/${
tenantName || ""
}/monitoring`
)
.then((res: ITenantMonitoringStruct) => {
dispatch(setPrometheusEnabled(res.prometheusEnabled));
setMonitoringInfo(res);
setRefreshMonitoringInfo(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setRefreshMonitoringInfo(false);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refreshMonitoringInfo]);
const submitMonitoringInfo = () => {
if(checkValid()){
api
.invoke(
"PUT",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/monitoring`,
{
labels: trim(labels),
annotations: trim(annotations),
nodeSelector: trim(nodeSelector),
image: image,
sidecarImage: sidecarImage,
initImage: initImage,
diskCapacityGB: diskCapacityGB,
serviceAccountName: serviceAccountName,
storageClassName: storageClassName,
monitoringCPURequest: cpuRequest,
monitoringMemRequest: memRequest + "Gi",
}
)
.then(() => {
setRefreshMonitoringInfo(true);
dispatch(setSnackBarMessage(`Prometheus configuration updated.`));
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err)
});
}
};
const togglePrometheus = () => {
const configInfo = {
prometheusEnabled: prometheusEnabled ,
toggle: true,
};
api
.invoke(
"PUT",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/monitoring`,
configInfo
)
.then(() => {
dispatch(setPrometheusEnabled(!prometheusEnabled));
setRefreshMonitoringInfo(true);
setToggleConfirmOpen(false);
setRefreshMonitoringInfo(true);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
});
};
return (
<Fragment>
{toggleConfirmOpen && (
<ConfirmDialog
isOpen={toggleConfirmOpen}
title={
!prometheusEnabled
? "Enable Prometheus monitoring for this tenant?"
: "Disable Prometheus monitoring for this tenant?"
}
confirmText={!prometheusEnabled ? "Enable" : "Disable"}
cancelText="Cancel"
onClose={() => setToggleConfirmOpen(false)}
onConfirm={togglePrometheus}
confirmationContent={
<DialogContentText>
{!prometheusEnabled
? "A small Prometheus server will be started per the configuration provided, which will collect the Prometheus metrics for your tenant."
: " Current configuration will be lost, and defaults reset if reenabled."}
</DialogContentText>
}
/>
)}
<Grid container spacing={1}>
<Grid item xs>
<h1 className={classes.sectionTitle}>Prometheus Monitoring </h1>
</Grid>
<Grid item xs={7} justifyContent={"end"} textAlign={"right"}>
<FormSwitchWrapper
label={""}
indicatorLabels={["Enabled", "Disabled"]}
checked={prometheusEnabled}
value={"tenant_monitoring"}
id="tenant-monitoring"
name="tenant-monitoring"
onChange={() => {
setToggleConfirmOpen(true);
}}
description=""
/>
</Grid>
<Grid xs={12}>
<hr className={classes.hrClass} />
</Grid>
</Grid>
{prometheusEnabled && (
<Fragment>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`image`}
label={"Image"}
placeholder={"quay.io/prometheus/prometheus:latest"}
name={`image`}
value={image}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setImage(event.target.value));
}
cleanValidation(`image`)
}}
key={`image`}
pattern={"^[a-zA-Z0-9-./:]{1,253}$"}
error={validationErrors[`image`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`sidecarImage`}
label={"Sidecar Image"}
placeholder={"library/alpine:latest"}
name={`sidecarImage`}
value={sidecarImage}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setSidecarImage(event.target.value));
}
cleanValidation(`sidecarImage`)
}}
key={`sidecarImage`}
pattern={"^[a-zA-Z0-9-./:]{1,253}$"}
error={validationErrors[`sidecarImage`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`initImage`}
label={"Init Image"}
placeholder={"library/busybox:1.33.1"}
name={`initImage`}
value={initImage}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setInitImage(event.target.value));
}
cleanValidation(`initImage`)
}}
key={`initImage`}
pattern={"^[a-zA-Z0-9-./:]{1,253}$"}
error={validationErrors[`initImage`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`diskCapacityGB`}
label={"Disk Capacity"}
placeholder={"Disk Capacity"}
name={`diskCapacityGB`}
value={diskCapacityGB}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setDiskCapacityGB(event.target.value));
}
cleanValidation(`diskCapacityGB`)
}}
key={`diskCapacityGB`}
pattern={"[0-9]*"}
error={validationErrors[`diskCapacityGB`] || ""}
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`cpuRequest`}
label={"CPU Request"}
placeholder={"CPU Request"}
name={`cpuRequest`}
value={cpuRequest}
pattern={"[0-9]*"}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setCPURequest(event.target.value));
}
cleanValidation(`cpuRequest`)
}}
key={`cpuRequest`}
error={validationErrors[`cpuRequest`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`memRequest`}
label={"Memory Request"}
placeholder={"Memory request"}
name={`memRequest`}
value={memRequest}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setMemRequest(event.target.value));
}
cleanValidation(`memRequest`)
}}
pattern={"[0-9]*"}
key={`memRequest`}
error={validationErrors[`memRequest`] || ""}
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`serviceAccountName`}
label={"Service Account"}
placeholder={"Service Account Name"}
name={`serviceAccountName`}
value={serviceAccountName}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setServiceAccountName(event.target.value));
}
cleanValidation(`serviceAccountName`)
}
}
key={`serviceAccountName`}
pattern={"^[a-zA-Z0-9-.]{1,253}$"}
error={validationErrors[`serviceAccountName`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`storageClassName`}
label={"Storage Class"}
placeholder={"Storage Class Name"}
name={`storageClassName`}
value={storageClassName}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setStorageClassName(event.target.value));
}
cleanValidation(`storageClassName`)
}}
key={`storageClassName`}
pattern={"^[a-zA-Z0-9-.]{1,253}$"}
error={validationErrors[`storageClassName`] || ""}
/>
</Grid>
{labels !== null &&
<Grid item xs={12} className={classes.formFieldRow}>
<span className={classes.inputLabel}>Labels</span>
<KeyPairEdit
newValues={labels}
setNewValues={setLabels}
paramName={"Labels"}
error={labelsError}
setError={setLabelsError}
/>
</Grid>}
{annotations !== null &&
<Grid item xs={12} className={classes.formFieldRow}>
<span className={classes.inputLabel}>Annotations</span>
<KeyPairEdit
newValues={annotations}
setNewValues={setAnnotations}
paramName={"Annotations"}
error={annotationsError}
setError={setAnnotationsError}
/>
</Grid>
}
{nodeSelector !== null &&
<Grid item xs={12} className={classes.formFieldRow}>
<span className={classes.inputLabel}>Node Selector</span>
<KeyPairEdit
newValues={nodeSelector}
setNewValues={setNodeSelector}
paramName={"Node Selector"}
error={nodeSelectorError}
setError={setNodeSelectorError}
/>
</Grid>
}
<Grid item xs={12} textAlign={"right"}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={!checkValid()}
onClick={() =>
submitMonitoringInfo()
}
>
Save
</Button>
</Grid>
</Fragment>
)}
</Fragment>
);
};
export default withStyles(styles)(TenantMonitoring);

View File

@@ -13,6 +13,7 @@ import {
IValidation,
} from "../../../../utils/validationFunctions";
import { clearValidationError } from "../utils";
import Grid from "@mui/material/Grid";
interface IKeyPairEditProps {
classes: any;
@@ -81,8 +82,10 @@ const KeyPairEdit = ({
let keyValueInputs = newValues.map((_, index) => {
return (
<Fragment key={`keyvalue-${index.toString()}`}>
<div className={classes.shortened}>
<Fragment key={`keyvalue-${index.toString()}`} >
<Grid paddingBottom={1}>
<div className={classes.shortened} >
<InputBoxWrapper
id={`key-${index.toString()}`}
label={""}
@@ -146,6 +149,7 @@ const KeyPairEdit = ({
</IconButton>
</Tooltip>
</div>
</Grid>
</Fragment>
);
});

View File

@@ -83,8 +83,8 @@ const DeleteTenant = withSuspense(
React.lazy(() => import("../ListTenants/DeleteTenant"))
);
const PodDetails = withSuspense(React.lazy(() => import("./pods/PodDetails")));
const TenantMonitoring = withSuspense(
React.lazy(() => import("./TenantMonitoring"))
const EditTenantMonitoringScreen = withSuspense(
React.lazy(() => import("./EditTenantMonitoringScreen"))
);
interface ITenantDetailsProps {
@@ -391,7 +391,7 @@ const TenantDetails = ({ classes }: ITenantDetailsProps) => {
<Route path={"pvcs/:PVCName"} element={<TenantVolumes />} />
<Route path={"volumes"} element={<VolumesSummary />} />
<Route path={"license"} element={<TenantLicense />} />
<Route path={"monitoring"} element={<TenantMonitoring />} />
<Route path={"monitoring"} element={<EditTenantMonitoringScreen />} />
<Route path={"logging"} element={<TenantLogging />} />
<Route path={"events"} element={<TenantEvents />} />
<Route path={"csr"} element={<TenantCSR />} />

View File

@@ -1,349 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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 { useSelector } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import Grid from "@mui/material/Grid";
import {
actionsTray,
containerForHeader,
searchField,
tenantDetailsStyles,
} from "../../Common/FormComponents/common/styleLibrary";
import { DialogContentText } from "@mui/material";
import Paper from "@mui/material/Paper";
import { ITenantMonitoringStruct } from "../ListTenants/types";
import { ErrorResponseHandler } from "../../../../common/types";
import EditTenantMonitoringModal from "./EditTenantMonitoringModal";
import api from "../../../../common/api";
import { EditIcon } from "../../../../icons";
import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import KeyPairView from "./KeyPairView";
import { niceBytes } from "../../../../common/utils";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import RBIconButton from "../../Buckets/BucketDetails/SummaryItems/RBIconButton";
import Loader from "../../Common/Loader/Loader";
import { setErrorSnackMessage } from "../../../../systemSlice";
import { useParams } from "react-router-dom";
import { AppState, useAppDispatch } from "../../../../store";
interface ITenantMonitoring {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
...tenantDetailsStyles,
paperContainer: {
padding: "15px 15px 15px 50px",
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});
const TenantMonitoring = ({ classes }: ITenantMonitoring) => {
const dispatch = useAppDispatch();
const { tenantName, tenantNamespace } = useParams();
const loadingTenant = useSelector(
(state: AppState) => state.tenants.loadingTenant
);
const tenant = useSelector((state: AppState) => state.tenants.tenantInfo);
const [prometheusMonitoringEnabled, setPrometheusMonitoringEnabled] =
useState<boolean>(false);
const [edit, setEdit] = useState<boolean>(false);
const [monitoringInfo, setMonitoringInfo] =
useState<ITenantMonitoringStruct>();
const [confirmOpen, setConfirmOpen] = useState<boolean>(false);
const [refreshMonitoringInfo, setRefreshMonitoringInfo] =
useState<boolean>(true);
const onCloseEditAndRefresh = () => {
setEdit(false);
setRefreshMonitoringInfo(true);
};
useEffect(() => {
if (refreshMonitoringInfo) {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace || ""}/tenants/${
tenantName || ""
}/monitoring`
)
.then((res: ITenantMonitoringStruct) => {
setPrometheusMonitoringEnabled(res.prometheusEnabled);
setMonitoringInfo(res);
setRefreshMonitoringInfo(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setRefreshMonitoringInfo(false);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refreshMonitoringInfo]);
const togglePrometheus = () => {
const configInfo = {
prometheusEnabled: prometheusMonitoringEnabled,
toggle: true,
};
api
.invoke(
"PUT",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/monitoring`,
configInfo
)
.then(() => {
setPrometheusMonitoringEnabled(!prometheusMonitoringEnabled);
setRefreshMonitoringInfo(true);
setConfirmOpen(false);
setRefreshMonitoringInfo(true);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
});
};
return (
<Fragment>
{edit && tenant !== null && prometheusMonitoringEnabled && (
<EditTenantMonitoringModal
classes={classes}
open={edit}
onClose={onCloseEditAndRefresh}
tenant={tenant}
image={monitoringInfo?.image || ""}
sidecarImage={monitoringInfo?.sidecarImage || ""}
initImage={monitoringInfo?.initImage || ""}
diskCapacityGB={
monitoringInfo?.diskCapacityGB
? parseInt(monitoringInfo?.diskCapacityGB)
: 5
}
labels={monitoringInfo?.labels || []}
annotations={monitoringInfo?.annotations || []}
nodeSelector={monitoringInfo?.nodeSelector || []}
serviceAccountName={monitoringInfo?.serviceAccountName || ""}
tenantName={tenantName || ""}
tenantNamespace={tenantNamespace || ""}
storageClassName={monitoringInfo?.storageClassName || ""}
cpuRequest={monitoringInfo?.monitoringCPURequest || ""}
memRequest={monitoringInfo?.monitoringMemRequest || ""}
/>
)}
{confirmOpen && (
<ConfirmDialog
isOpen={confirmOpen}
title={
prometheusMonitoringEnabled
? "Disable Prometheus monitoring?"
: "Enable Prometheus monitoring?"
}
confirmText={prometheusMonitoringEnabled ? "Disable" : "Enable"}
cancelText="Cancel"
onClose={() => setConfirmOpen(false)}
onConfirm={togglePrometheus}
confirmationContent={
<DialogContentText>
{prometheusMonitoringEnabled
? "Disabling monitoring will erase any custom values you have used to configure Prometheus monitoring"
: "Prometheus monitoring will be enabled with default values"}
</DialogContentText>
}
/>
)}
<Grid container alignItems={"center"}>
<Grid item xs>
<h1 className={classes.sectionTitle}>Monitoring</h1>
</Grid>
<Grid item xs={4}>
<FormSwitchWrapper
indicatorLabels={["Enabled", "Disabled"]}
checked={prometheusMonitoringEnabled}
value={"monitoring_status"}
id="monitoring-status"
name="monitoring-status"
onChange={(e) => {
setConfirmOpen(true);
}}
description=""
/>
</Grid>
</Grid>
{prometheusMonitoringEnabled && monitoringInfo !== undefined && (
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={12}>
<Grid container alignItems={"center"}>
<Grid xs={8}>
<h3>Configuration</h3>
</Grid>
<Grid xs={4} justifyContent={"end"} textAlign={"right"}>
<RBIconButton
tooltip={"Edit Monitoring configuration"}
text={"Edit"}
onClick={() => {
setEdit(true);
}}
icon={<EditIcon />}
color="primary"
variant={"contained"}
/>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<hr className={classes.hrClass} />
<table width={"100%"}>
<tbody>
{loadingTenant ? (
<tr>
<td className={classes.centerAlign} colSpan={4}>
<Loader />
</td>
</tr>
) : (
<Fragment>
{monitoringInfo.image != null && (
<tr>
<td className={classes.titleCol}>Image:</td>
<td>{monitoringInfo.image}</td>
</tr>
)}
{monitoringInfo.sidecarImage != null && (
<tr>
<td className={classes.titleCol}>Sidecar Image:</td>
<td>{monitoringInfo?.sidecarImage}</td>
</tr>
)}
{monitoringInfo.initImage != null && (
<tr>
<td className={classes.titleCol}>Init Image:</td>
<td>{monitoringInfo?.initImage}</td>
</tr>
)}
{monitoringInfo.diskCapacityGB != null && (
<tr>
<td className={classes.titleCol}>
Disk Capacity (GB):
</td>
<td>{monitoringInfo?.diskCapacityGB}</td>
</tr>
)}
{monitoringInfo.serviceAccountName != null && (
<tr>
<td className={classes.titleCol}>
Service Account Name:
</td>
<td>{monitoringInfo?.serviceAccountName}</td>
</tr>
)}
{monitoringInfo.storageClassName != null && (
<tr>
<td className={classes.titleCol}>
Storage Class Name:
</td>
<td>{monitoringInfo?.storageClassName}</td>
</tr>
)}
{monitoringInfo.labels != null &&
monitoringInfo.labels.length > 0 && (
<>
<tr>
<td>
<h4>Labels</h4>
</td>
</tr>
<tr>
<td className={classes.titleCol}>
<KeyPairView
records={monitoringInfo.labels}
recordName="Labels"
/>
</td>
</tr>
</>
)}
{monitoringInfo.annotations != null &&
monitoringInfo.annotations.length > 0 && (
<>
<tr>
<td>
<h4>Annotations</h4>
</td>
</tr>
<tr>
<td className={classes.titleCol}>
<KeyPairView
records={monitoringInfo.annotations}
recordName="Annotations"
/>
</td>
</tr>
</>
)}
{monitoringInfo.monitoringCPURequest != null && (
<tr>
<td className={classes.titleCol}>CPU Request:</td>
<td>{monitoringInfo?.monitoringCPURequest}</td>
</tr>
)}
{monitoringInfo.monitoringMemRequest != null && (
<tr>
<td className={classes.titleCol}>Memory Request:</td>
<td>
{niceBytes(
monitoringInfo?.monitoringMemRequest,
true
)}
</td>
</tr>
)}
{monitoringInfo.nodeSelector != null &&
monitoringInfo.nodeSelector.length > 0 && (
<tr>
<h4>Node Selector:</h4>
<td className={classes.titleCol}>
<KeyPairView
records={monitoringInfo.nodeSelector}
recordName="Node Selector"
/>
</td>
</tr>
)}
</Fragment>
)}
</tbody>
</table>
</Grid>
</Grid>
</Paper>
)}
</Fragment>
);
};
export default withStyles(styles)(TenantMonitoring);

View File

@@ -0,0 +1,107 @@
// 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 { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { IKeyValue } from "../ListTenants/types";
export interface IEditTenantMonitoring {
prometheusEnabled: boolean;
image: string;
sidecarImage: string;
initImage: string;
storageClassName: string;
labels: IKeyValue[];
annotations: IKeyValue[];
nodeSelector: IKeyValue[];
diskCapacityGB: string;
serviceAccountName: string;
monitoringCPURequest: string;
monitoringMemRequest: string;
}
const initialState: IEditTenantMonitoring = {
prometheusEnabled: false,
image: "",
sidecarImage: "",
initImage: "",
storageClassName: "",
labels: [{key:" ",value:" "}],
annotations: [{key:" ",value:" "}],
nodeSelector: [{key:" ",value:" "}],
diskCapacityGB: "0",
serviceAccountName: "",
monitoringCPURequest: "",
monitoringMemRequest: "",
};
export const editTenantMonitoringSlice = createSlice({
name: "editTenantMonitoring",
initialState,
reducers: {
setPrometheusEnabled: (state, action: PayloadAction<boolean>) => {
state.prometheusEnabled = action.payload;
},
setImage: (state, action: PayloadAction<string>) => {
state.image = action.payload;
},
setSidecarImage:(state, action: PayloadAction<string>) => {
state.sidecarImage = action.payload;
},
setInitImage: (state, action: PayloadAction<string>) => {
state.initImage = action.payload;
},
setStorageClassName: (state, action: PayloadAction<string>) => {
state.storageClassName = action.payload;
},
setLabels: (state, action: PayloadAction<IKeyValue[]>) => {
state.labels = action.payload;
},
setAnnotations: (state, action: PayloadAction<IKeyValue[]>) => {
state.annotations = action.payload;
},
setNodeSelector: (state, action: PayloadAction<IKeyValue[]>) => {
state.nodeSelector = action.payload;
},
setDiskCapacityGB: (state, action: PayloadAction<string>) => {
state.diskCapacityGB = action.payload;
},
setServiceAccountName: (state, action: PayloadAction<string>) => {
state.serviceAccountName = action.payload;
},
setCPURequest: (state, action: PayloadAction<string>) => {
state.monitoringCPURequest = action.payload;
},
setMemRequest: (state, action: PayloadAction<string>) => {
state.monitoringMemRequest = action.payload;
},
},
});
export const {
setPrometheusEnabled,
setImage,
setSidecarImage,
setInitImage,
setStorageClassName,
setLabels,
setAnnotations,
setNodeSelector,
setDiskCapacityGB,
setServiceAccountName,
setCPURequest,
setMemRequest,
} = editTenantMonitoringSlice.actions;
export default editTenantMonitoringSlice.reducer;

View File

@@ -31,6 +31,7 @@ import createTenantReducer from "./screens/Console/Tenants/AddTenant/createTenan
import createUserReducer from "./screens/Console/Users/AddUsersSlice";
import addPoolReducer from "./screens/Console/Tenants/TenantDetails/Pools/AddPool/addPoolSlice";
import editPoolReducer from "./screens/Console/Tenants/TenantDetails/Pools/EditPool/editPoolSlice";
import editTenantMonitoringReducer from "./screens/Console/Tenants/TenantDetails/tenantMonitoringSlice"
const rootReducer = combineReducers({
system: systemReducer,
@@ -49,6 +50,7 @@ const rootReducer = combineReducers({
createUser: createUserReducer,
addPool: addPoolReducer,
editPool: editPoolReducer,
editTenantMonitoring: editTenantMonitoringReducer,
});
export const store = configureStore({