diff --git a/operatorapi/tenants.go b/operatorapi/tenants.go index 41a017bdf..eee3d2f02 100644 --- a/operatorapi/tenants.go +++ b/operatorapi/tenants.go @@ -1639,7 +1639,6 @@ func enableTenantLoggingResponse(session *models.Principal, params operator_api. return false, restapi.ErrorWithContext(ctx, err, restapi.ErrUnableToGetTenantUsage) } minTenant.EnsureDefaults() - // Default class name for Log search diskSpaceFromAPI := int64(5) * humanize.GiByte // Default is 5Gi logSearchStorageClass := "standard" diff --git a/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts b/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts index 6ec4be422..19e0c7f8d 100644 --- a/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts +++ b/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts @@ -220,6 +220,7 @@ export interface ITenantMonitoringStruct { } export interface ITenantLogsStruct { + auditLoggingEnabled: boolean; image: string; labels: IKeyValue[]; annotations: IKeyValue[]; diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/EditTenantLogsModal.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/EditTenantLogsModal.tsx deleted file mode 100644 index a88074777..000000000 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/EditTenantLogsModal.tsx +++ /dev/null @@ -1,597 +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 . - -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 { clearValidationError } from "../utils"; - -import InputUnitMenu from "../../Common/FormComponents/InputUnitMenu/InputUnitMenu"; - -import { setModalErrorSnackMessage } from "../../../../systemSlice"; -import { useAppDispatch } from "../../../../store"; - -interface IEditTenantLogsProps { - tenant: ITenant; - classes: any; - open: boolean; - onClose: (shouldReload: boolean) => void; - image: string; - labels: IKeyValue[]; - annotations: IKeyValue[]; - nodeSelector: IKeyValue[]; - diskCapacityGB: number; - serviceAccountName: string; - dbImage: string; - dbInitImage: string; - dbLabels: IKeyValue[]; - dbAnnotations: IKeyValue[]; - dbNodeSelector: IKeyValue[]; - dbServiceAccountName: string; - cpuRequest: string; - memRequest: string; - dbCPURequest: string; - dbMemRequest: string; -} - -const styles = (theme: Theme) => - createStyles({ - buttonContainer: { - textAlign: "right", - }, - bottomContainer: { - display: "flex", - flexGrow: 1, - alignItems: "center", - "& div": { - flexGrow: 1, - width: "100%", - }, - }, - ...modalBasic, - ...modalStyleUtils, - ...formFieldStyles, - }); - -const EditTenantLogsModal = ({ - tenant, - classes, - open, - onClose, - image, - labels, - annotations, - nodeSelector, - diskCapacityGB, - serviceAccountName, - dbLabels, - dbAnnotations, - dbNodeSelector, - dbImage, - dbInitImage, - dbServiceAccountName, - cpuRequest, - memRequest, - dbCPURequest, - dbMemRequest, -}: IEditTenantLogsProps) => { - const dispatch = useAppDispatch(); - const [validationErrors, setValidationErrors] = useState({}); - const [newLabels, setNewLabels] = useState( - labels.length > 0 ? [...labels] : [{ key: "", value: "" }] - ); - const [newAnnotations, setNewAnnotations] = useState( - annotations.length > 0 ? [...annotations] : [{ key: "", value: "" }] - ); - const [newNodeSelector, setNewNodeSelector] = useState( - nodeSelector.length > 0 ? [...nodeSelector] : [{ key: "", value: "" }] - ); - const [newImage, setNewImage] = useState(image); - const [newDiskCapacityGB, setNewDiskCapacityGB] = - useState(diskCapacityGB); - const [newServiceAccountName, setNewServiceAccountName] = useState( - serviceAccountName != null ? serviceAccountName : "" - ); - const [newDbLabels, setNewDbLabels] = useState( - dbLabels.length > 0 ? [...dbLabels] : [{ key: "", value: "" }] - ); - const [newDbAnnotations, setNewDbAnnotations] = useState( - dbAnnotations.length > 0 ? [...dbAnnotations] : [{ key: "", value: "" }] - ); - const [newDbNodeSelector, setNewDbNodeSelector] = useState( - dbNodeSelector.length > 0 ? [...dbNodeSelector] : [{ key: "", value: "" }] - ); - const [newDbImage, setNewDbImage] = useState(dbImage); - const [newDbInitImage, setNewDbInitImage] = useState(dbInitImage); - const [newDbServiceAccountName, setNewDbServiceAccountName] = - useState(dbServiceAccountName != null ? dbServiceAccountName : ""); - const [labelsError, setLabelsError] = useState({}); - const [annotationsError, setAnnotationsError] = useState({}); - const [nodeSelectorError, setNodeSelectorError] = useState({}); - const [dbLabelsError, setDbLabelsError] = useState({}); - const [dbAnnotationsError, setDbAnnotationsError] = useState({}); - const [dbNodeSelectorError, setDbNodeSelectorError] = useState({}); - const [newCPURequest, setNewCPURequest] = useState(cpuRequest); - const [newMemRequest, setNewMemRequest] = useState( - memRequest - ? Math.floor(parseInt(memRequest, 10) / 1000000000).toString() - : "0" - ); - const [newDBCPURequest, setNewDBCPURequest] = useState(dbCPURequest); - const [newDBMemRequest, setNewDBMemRequest] = useState( - dbMemRequest - ? Math.floor(parseInt(dbMemRequest, 10) / 1000000000).toString() - : "0" - ); - - 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 cleanValidation = (fieldName: string) => { - setValidationErrors(clearValidationError(validationErrors, fieldName)); - }; - - useEffect(() => { - let tenantLogValidation: IValidation[] = []; - - tenantLogValidation.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", - }); - tenantLogValidation.push({ - fieldKey: `dbImage`, - required: false, - value: newDbImage, - 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", - }); - tenantLogValidation.push({ - fieldKey: `dbInitImage`, - required: false, - value: newDbInitImage, - 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", - }); - tenantLogValidation.push({ - fieldKey: `diskCapacityGB`, - required: true, - value: newDiskCapacityGB as any as string, - pattern: /^[0-9]*$/, - customPatternMessage: "Must be an integer between 0 and 10", - }); - tenantLogValidation.push({ - fieldKey: `serviceAccountName`, - required: false, - value: newServiceAccountName, - pattern: /^[a-zA-Z0-9-.]{1,253}$/, - customPatternMessage: "Invalid service account name", - }); - tenantLogValidation.push({ - fieldKey: `dbServiceAccountName`, - required: false, - value: newDbServiceAccountName, - pattern: /^[a-zA-Z0-9-.]{1,253}$/, - customPatternMessage: "Invalid service account name", - }); - tenantLogValidation.push({ - fieldKey: `cpuRequest`, - required: true, - value: newCPURequest as any as string, - pattern: /^[0-9]*$/, - customPatternMessage: - "Please enter an integer value for number of CPUs requested", - }); - tenantLogValidation.push({ - fieldKey: `memRequest`, - required: true, - value: newMemRequest as any as string, - pattern: /^[0-9]*$/, - customPatternMessage: - "Please enter an integer value (Gi) for memory requested", - }); - tenantLogValidation.push({ - fieldKey: `dbCPURequest`, - required: true, - value: newDBCPURequest as any as string, - pattern: /^[0-9]*$/, - customPatternMessage: - "Please enter an integer value for number of DB CPUs requested", - }); - tenantLogValidation.push({ - fieldKey: `dbMemRequest`, - required: true, - value: newDBMemRequest as any as string, - pattern: /^[0-9]*$/, - customPatternMessage: - "Please enter an integer value (Gi) for DB memory requested", - }); - - const commonVal = commonFormValidation(tenantLogValidation); - setValidationErrors(commonVal); - }, [ - newImage, - newDbImage, - newDbInitImage, - newDiskCapacityGB, - newServiceAccountName, - newDbServiceAccountName, - newCPURequest, - newMemRequest, - newDBCPURequest, - newDBMemRequest, - 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 || - Object.keys(dbLabelsError).length !== 0 || - Object.keys(dbAnnotationsError).length !== 0 || - Object.keys(dbNodeSelectorError).length !== 0 - ) { - return false; - } else { - return true; - } - }; - - return ( - onClose(true)} - modalOpen={open} - title="Edit Logging" - > -
) => { - e.preventDefault(); - if (!checkValid()) { - dispatch( - setModalErrorSnackMessage({ - errorMessage: "Some fields have invalid values", - detailedError: "", - }) - ); - } else { - api - .invoke( - "PUT", - `/api/v1/namespaces/${tenant.namespace}/tenants/${tenant.name}/log`, - { - labels: trim(newLabels), - annotations: trim(newAnnotations), - nodeSelector: trim(newNodeSelector), - image: newImage, - diskCapacityGB: newDiskCapacityGB, - serviceAccountName: newServiceAccountName, - dbLabels: trim(newDbLabels), - dbAnnotations: trim(newDbAnnotations), - dbNodeSelector: trim(newDbNodeSelector), - dbImage: newDbImage, - dbInitImage: newDbInitImage, - dbServiceAccountName: newDbServiceAccountName, - logCPURequest: newCPURequest, - logMemRequest: newMemRequest + "Gi", - logDBCPURequest: newDBCPURequest, - logDBMemRequest: newDBMemRequest + "Gi", - } - ) - .then(() => { - onClose(true); - }) - .catch((err: ErrorResponseHandler) => {}); - } - }} - > - - - -

Logging API

-
- - { - setNewImage(e.target.value); - cleanValidation(`image`); - }} - key={`image`} - error={validationErrors[`image`] || ""} - /> - - - { - setNewDiskCapacityGB(e.target.value as any as number); - cleanValidation(`diskCapacityGB`); - }} - key={`diskCapacityGB`} - error={validationErrors[`diskCapacityGB`] || ""} - overlayObject={ - {}} - unitSelected={"Gi"} - unitsList={[{ label: "Gi", value: "Gi" }]} - disabled={true} - /> - } - /> - - - { - setNewServiceAccountName(e.target.value); - cleanValidation(`serviceAccountName`); - }} - key={`serviceAccountName`} - error={validationErrors[`serviceAccountName`] || ""} - /> - - - - { - setNewCPURequest(e.target.value as any as string); - cleanValidation(`cpuRequest`); - }} - key={`cpuRequest`} - error={validationErrors[`cpuRequest`] || ""} - /> - - - { - setNewMemRequest(e.target.value as any as string); - cleanValidation(`memRequest`); - }} - key={`memRequest`} - error={validationErrors[`memRequest`] || ""} - overlayObject={ - {}} - unitSelected={"Gi"} - unitsList={[{ label: "Gi", value: "Gi" }]} - disabled={true} - /> - } - /> - - - - Labels - - - - Annotations - - - - Node Selector - - - -

Database Configuration

-
- - { - setNewDbImage(e.target.value); - cleanValidation(`dbImage`); - }} - key={`dbImage`} - error={validationErrors[`dbImage`] || ""} - /> - - - { - setNewDbInitImage(e.target.value); - cleanValidation(`dbInitImage`); - }} - key={`dbInitImage`} - error={validationErrors[`dbInitImage`] || ""} - /> - - - { - setNewDbServiceAccountName(e.target.value); - cleanValidation(`dbServiceAccountName`); - }} - key={`dbServiceAccountName`} - error={validationErrors[`dbServiceAccountName`] || ""} - /> - - - { - setNewDBCPURequest(e.target.value as any as string); - cleanValidation(`dbCpuRequest`); - }} - key={`dbCpuRequest`} - error={validationErrors[`dbCpuRequest`] || ""} - /> - - - { - setNewDBMemRequest(e.target.value as any as string); - cleanValidation(`dbMemRequest`); - }} - key={`dbMemRequest`} - error={validationErrors[`dbMemRequest`] || ""} - overlayObject={ - {}} - unitSelected={"Gi"} - unitsList={[{ label: "Gi", value: "Gi" }]} - disabled={true} - /> - } - /> - - - Labels - - - - Annotations - - - - Node Selector - - -
- - - -
-
-
- ); -}; - -export default withStyles(styles)(EditTenantLogsModal); diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantAuditLogsScreen.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantAuditLogsScreen.tsx new file mode 100644 index 000000000..1602431d6 --- /dev/null +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantAuditLogsScreen.tsx @@ -0,0 +1,655 @@ +// 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 . + +//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 { ITenantLogsStruct } from "../ListTenants/types"; +import { + setAuditLoggingEnabled, + setImage, + setDBImage, + setDBInitImage, + setDiskCapacityGB, + setServiceAccountName, + setDBServiceAccountName, + setCPURequest, + setMemRequest, + setDBCPURequest, + setDBMemRequest, +} from "../TenantDetails/tenantAuditLogSlice"; + +import { clearValidationError } from "../utils"; + +interface ITenantAuditLogs { + 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 TenantAuditLogging = ({ classes }: ITenantAuditLogs) => { + const dispatch = useAppDispatch(); + const { tenantName, tenantNamespace } = useParams(); + const auditLoggingEnabled = useSelector( + (state: AppState) => state.editTenantLogging.auditLoggingEnabled + ); + const image = useSelector( + (state: AppState) => state.editTenantLogging.image + ); + const dbImage = useSelector( + (state: AppState) => state.editTenantLogging.dbImage + ); + const dbInitImage = useSelector( + (state: AppState) => state.editTenantLogging.dbInitImage + ); + const diskCapacityGB = useSelector( + (state: AppState) => state.editTenantLogging.diskCapacityGB + ); + const cpuRequest = useSelector( + (state: AppState) => state.editTenantLogging.cpuRequest + ); + const memRequest = useSelector( + (state: AppState) => state.editTenantLogging.memRequest + ); + + const dbCpuRequest = useSelector( + (state: AppState) => state.editTenantLogging.dbCPURequest + ); + const dbMemRequest = useSelector( + (state: AppState) => state.editTenantLogging.dbMemRequest + ); + const serviceAccountName = useSelector( + (state: AppState) => state.editTenantLogging.serviceAccountName + ); + const dbServiceAccountName = useSelector( + (state: AppState) => state.editTenantLogging.dbServiceAccountName + ); + const [validationErrors, setValidationErrors] = useState({}); + const [toggleConfirmOpen, setToggleConfirmOpen] = useState(false); + + const [labels, setLabels] = useState([{ key: "", value: "" }]); + const [annotations, setAnnotations] = useState([ + { key: "", value: "" }, + ]); + const [nodeSelector, setNodeSelector] = useState([ + { key: "", value: "" }, + ]); + const [dbLabels, setDBLabels] = useState([{ key: "", value: "" }]); + const [dbAnnotations, setDBAnnotations] = useState([ + { key: "", value: "" }, + ]); + const [dbNodeSelector, setDBNodeSelector] = useState([ + { key: "", value: "" }, + ]); + + const [refreshLoggingInfo, setRefreshLoggingInfo] = + useState(true); + const [labelsError, setLabelsError] = useState({}); + const [annotationsError, setAnnotationsError] = useState({}); + const [nodeSelectorError, setNodeSelectorError] = useState({}); + + const [dbLabelsError, setDBLabelsError] = useState({}); + const [dbAnnotationsError, setDBAnnotationsError] = useState({}); + const [dbNodeSelectorError, setDBNodeSelectorError] = useState({}); + + const cleanValidation = (fieldName: string) => { + setValidationErrors(clearValidationError(validationErrors, fieldName)); + }; + + const setLoggingInfo = (res: ITenantLogsStruct) => { + dispatch(setAuditLoggingEnabled(!res.disabled)) + dispatch(setImage(res.image)); + dispatch(setServiceAccountName(res.serviceAccountName)); + dispatch(setDBServiceAccountName(res.dbServiceAccountName)); + dispatch(setDBImage(res.dbImage)); + dispatch(setDBInitImage(res.dbInitImage)); + dispatch(setCPURequest(res.logCPURequest)); + dispatch(setDBCPURequest(res.logDBCPURequest)); + if (res.logMemRequest) { + dispatch( + setMemRequest( + Math.floor( + parseInt(res.logMemRequest, 10) / 1000000000 + ).toString() + ) + ); + } else { + dispatch(setMemRequest("0")); + } + if (res.logDBMemRequest) { + dispatch( + setDBMemRequest( + Math.floor( + parseInt(res.logDBMemRequest, 10) / 1000000000 + ).toString() + ) + ); + } else { + dispatch(setDBMemRequest("0")); + } + dispatch(setDiskCapacityGB(res.diskCapacityGB)); + res.labels.length > 0 ? setLabels(res.labels) : setLabels([{ key: "test", value: "test" }]); + res.annotations.length > 0 ? setAnnotations(res.annotations) : setAnnotations([{ key: "", value: "" }]); + res.nodeSelector.length > 0 ? setNodeSelector(res.nodeSelector) : setNodeSelector([{ key: "", value: "" }]); + res.dbLabels.length > 0 ? setDBLabels(res.dbLabels) : setDBLabels([{ key: "", value: "" }]); + res.dbAnnotations.length > 0 ? setDBAnnotations(res.dbAnnotations) : setDBAnnotations([{ key: "", value: "" }]); + res.dbNodeSelector.length > 0 ? setDBNodeSelector(res.dbNodeSelector) : setDBNodeSelector([{ 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 || + Object.keys(dbNodeSelectorError).length !== 0 || + Object.keys(dbAnnotationsError).length !== 0 || + Object.keys(dbLabelsError).length !== 0 + ) { + let err: ErrorResponseHandler = { + errorMessage: "Invalid entry", + detailedError: "", + }; + dispatch(setErrorSnackMessage(err)); + return false; + } else { + return true; + } + }; + + useEffect(() => { + if (refreshLoggingInfo) { + api + .invoke( + "GET", + `/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/log` + ) + .then((res: ITenantLogsStruct) => { + dispatch(setAuditLoggingEnabled(res.auditLoggingEnabled)); + setLoggingInfo(res); + setRefreshLoggingInfo(false); + }) + .catch((err: ErrorResponseHandler) => { + dispatch(setErrorSnackMessage(err)); + setRefreshLoggingInfo(false); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [refreshLoggingInfo]); + + const submitLoggingInfo = () => { + if (checkValid()) { + api + .invoke( + "PUT", + `/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/log`, + { + labels: trim(labels), + annotations: trim(annotations), + nodeSelector: trim(nodeSelector), + image: image, + diskCapacityGB: diskCapacityGB.toString(), + serviceAccountName: serviceAccountName, + dbLabels: trim(dbLabels), + dbAnnotations: trim(dbAnnotations), + dbNodeSelector: trim(dbNodeSelector), + dbImage: dbImage, + dbInitImage: dbInitImage, + dbServiceAccountName: dbServiceAccountName, + logCPURequest: cpuRequest, + logMemRequest: memRequest + "Gi", + logDBCPURequest: dbCpuRequest, + logDBMemRequest: dbMemRequest + "Gi", + } + ) + .then(() => { + setRefreshLoggingInfo(true); + dispatch(setSnackBarMessage(`Audit Log configuration updated.`)); + }) + .catch((err: ErrorResponseHandler) => { + setErrorSnackMessage(err); + }); + } + }; + + const toggleLogging = () => { + if(!auditLoggingEnabled) { + api + .invoke( + "POST", + `/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/enable-logging` + ) + .then(() => { + setRefreshLoggingInfo(true); + setToggleConfirmOpen(false); + }) + .catch((err: ErrorResponseHandler) => { + dispatch( + setErrorSnackMessage({ + errorMessage: "Error enabling logging", + detailedError: err.detailedError, + }) + ); + }); + } else { + api + .invoke( + "POST", + `/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/disable-logging` + ) + .then(() => { + setRefreshLoggingInfo(true); + setToggleConfirmOpen(false); + }) + .catch((err: ErrorResponseHandler) => { + dispatch( + setErrorSnackMessage({ + errorMessage: "Error disabling logging", + detailedError: err.detailedError, + }) + ); + }); + }; + }; + return ( + + {toggleConfirmOpen && ( + setToggleConfirmOpen(false)} + onConfirm={toggleLogging} + confirmationContent={ + + {!auditLoggingEnabled + ? "A small Postgres server will be started per the configuration provided, which will collect the audit logs for your tenant." + : " Current configuration will be lost, and defaults reset if reenabled."} + + } + /> + )} + + +

Audit Logs

+
+ + { + setToggleConfirmOpen(true); + }} + description="" + /> + + +
+
+
+ + {auditLoggingEnabled && ( + + + ) => { + if (event.target.validity.valid) { + dispatch(setImage(event.target.value)); + } + cleanValidation(`image`); + }} + key={`image`} + pattern={"^[a-zA-Z0-9-./:]{1,253}$"} + error={validationErrors[`image`] || ""} + /> + + + ) => { + if (event.target.validity.valid) { + dispatch(setDBImage(event.target.value)); + } + cleanValidation(`dbImage`); + }} + key={`dbImage`} + pattern={"^[a-zA-Z0-9-./:]{1,253}$"} + error={validationErrors[`dbImage`] || ""} + /> + + + ) => { + if (event.target.validity.valid) { + dispatch(setDBInitImage(event.target.value)); + } + cleanValidation(`dbInitImage`); + }} + key={`dbInitImage`} + pattern={"^[a-zA-Z0-9-./:]{1,253}$"} + error={validationErrors[`dbInitImage`] || ""} + /> + + + ) => { + if (event.target.validity.valid) { + dispatch(setDiskCapacityGB(parseInt(event.target.value))); + } + cleanValidation(`diskCapacityGB`); + }} + key={`diskCapacityGB`} + pattern={"[0-9]*"} + error={validationErrors[`diskCapacityGB`] || ""} + overlayObject={ + {}} + unitSelected={"Gi"} + unitsList={[{ label: "Gi", value: "Gi" }]} + disabled={true} + /> + } + /> + + + ) => { + if (event.target.validity.valid) { + dispatch(setCPURequest(event.target.value)); + } + cleanValidation(`cpuRequest`); + }} + key={`cpuRequest`} + error={validationErrors[`cpuRequest`] || ""} + /> + + + ) => { + if (event.target.validity.valid) { + dispatch(setDBCPURequest(event.target.value)); + } + cleanValidation(`dbCPURequest`); + }} + key={`dbCPURequest`} + error={validationErrors[`dbCPURequest`] || ""} + /> + + + ) => { + if (event.target.validity.valid) { + dispatch(setMemRequest(event.target.value)); + } + cleanValidation(`memRequest`); + }} + pattern={"[0-9]*"} + key={`memRequest`} + error={validationErrors[`memRequest`] || ""} + overlayObject={ + {}} + unitSelected={"Gi"} + unitsList={[{ label: "Gi", value: "Gi" }]} + disabled={true} + /> + } + /> + + + ) => { + if (event.target.validity.valid) { + dispatch(setDBMemRequest(event.target.value)); + } + cleanValidation(`dbMemRequest`); + }} + pattern={"[0-9]*"} + key={`dbMemRequest`} + error={validationErrors[`dbMemRequest`] || ""} + overlayObject={ + {}} + unitSelected={"Gi"} + unitsList={[{ label: "Gi", value: "Gi" }]} + disabled={true} + /> + } + /> + + + ) => { + if (event.target.validity.valid) { + dispatch(setServiceAccountName(event.target.value)); + } + cleanValidation(`serviceAccountName`); + }} + key={`serviceAccountName`} + pattern={"^[a-zA-Z0-9-.]{1,253}$"} + error={validationErrors[`serviceAccountName`] || ""} + /> + + {labels !== null && ( + + Labels + + + )} + + {annotations !== null && ( + + Annotations + + + )} + {nodeSelector !== null && ( + + Node Selector + + + )} + {dbLabels !== null && ( + + DB Labels + + + )} + + {dbAnnotations !== null && ( + + DB Annotations + + + )} + {dbNodeSelector !== null && ( + + DB Node Selector + + + )} + + + + + )} +
+ ); +}; + +export default withStyles(styles)(TenantAuditLogging); diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx index 3b10ca493..8ff101dca 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx @@ -59,7 +59,7 @@ const TenantSummary = withSuspense(React.lazy(() => import("./TenantSummary"))); const TenantLicense = withSuspense(React.lazy(() => import("./TenantLicense"))); const PoolsSummary = withSuspense(React.lazy(() => import("./PoolsSummary"))); const PodsSummary = withSuspense(React.lazy(() => import("./PodsSummary"))); -const TenantLogging = withSuspense(React.lazy(() => import("./TenantLogging"))); +const TenantLogging = withSuspense(React.lazy(() => import("./TenantAuditLogsScreen"))); const TenantEvents = withSuspense(React.lazy(() => import("./TenantEvents"))); const TenantCSR = withSuspense(React.lazy(() => import("./TenantCSR"))); const VolumesSummary = withSuspense( diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantLogging.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantLogging.tsx deleted file mode 100644 index 557a590c8..000000000 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantLogging.tsx +++ /dev/null @@ -1,500 +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 . - -import React, { Fragment, useEffect, useState } from "react"; -import { connect, useSelector } from "react-redux"; -import createStyles from "@mui/styles/createStyles"; -import withStyles from "@mui/styles/withStyles"; -import { Theme } from "@mui/material/styles"; -import { - actionsTray, - containerForHeader, - searchField, - tenantDetailsStyles, -} from "../../Common/FormComponents/common/styleLibrary"; -import Grid from "@mui/material/Grid"; -import { DialogContentText } from "@mui/material"; -import Paper from "@mui/material/Paper"; -import api from "../../../../common/api"; -import { ITenantLogsStruct } from "../ListTenants/types"; -import { AppState, useAppDispatch } from "../../../../store"; -import { ErrorResponseHandler } from "../../../../common/types"; -import { EditIcon } from "../../../../icons"; -import EditTenantLogsModal from "./EditTenantLogsModal"; -import KeyPairView from "./KeyPairView"; -import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog"; -import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; -import RBIconButton from "../../Buckets/BucketDetails/SummaryItems/RBIconButton"; -import { niceBytes } from "../../../../common/utils"; -import Loader from "../../Common/Loader/Loader"; -import { setErrorSnackMessage } from "../../../../systemSlice"; -import { useParams } from "react-router-dom"; - -interface ITenantLogs { - classes: any; -} - -const styles = (theme: Theme) => - createStyles({ - ...tenantDetailsStyles, - paperContainer: { - padding: "15px 15px 15px 50px", - }, - ...actionsTray, - ...searchField, - ...containerForHeader(theme.spacing(4)), - }); - -const TenantLogging = ({ classes }: ITenantLogs) => { - const dispatch = useAppDispatch(); - const params = useParams(); - - const loadingTenant = useSelector( - (state: AppState) => state.tenants.loadingTenant - ); - const tenant = useSelector((state: AppState) => state.tenants.tenantInfo); - - const [loadingTenantLogs, setLoadingTenantLogs] = useState(true); - const [logInfo, setLogInfo] = useState(); - const [edit, setEdit] = useState(false); - const [disabled, setDisabled] = useState(false); - const [preDisabled, setPreDisabled] = useState(false); - const [disableDialogOpen, setDisableDialogOpen] = useState(false); - const [enableDialogOpen, setEnableDialogOpen] = useState(false); - - const tenantName = params.tenantName; - const tenantNamespace = params.tenantNamespace; - - useEffect(() => { - if (loadingTenantLogs) { - api - .invoke( - "GET", - `/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/log` - ) - .then((result: ITenantLogsStruct) => { - setLogInfo(result); - setPreDisabled(result.disabled); - setDisabled(result.disabled); - setLoadingTenantLogs(false); - }) - .catch((err: ErrorResponseHandler) => { - dispatch( - setErrorSnackMessage({ - errorMessage: "Error getting tenant logs", - detailedError: err.detailedError, - }) - ); - }); - } - }, [ - tenantName, - tenantNamespace, - loadingTenantLogs, - setDisabled, - disabled, - dispatch, - ]); - - const onCloseEditAndRefresh = () => { - setDisableDialogOpen(false); - setEdit(false); - setLoadingTenantLogs(true); - }; - - const onCloseEnableAndRefresh = () => { - setEnableDialogOpen(false); - setDisabled(false); - setLoadingTenantLogs(true); - }; - - return ( - - { - api - .invoke( - "POST", - `/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/disable-logging` - ) - .then(() => { - setPreDisabled(true); - setDisabled(true); - }) - .catch((err: ErrorResponseHandler) => { - dispatch( - setErrorSnackMessage({ - errorMessage: "Error disabling logging", - detailedError: err.detailedError, - }) - ); - }); - onCloseEditAndRefresh(); - }} - onClose={() => setDisableDialogOpen(false)} - confirmationContent={ - - Disabling logging will erase any custom values you have used to - configure logging - - } - /> - { - api - .invoke( - "POST", - `/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/enable-logging` - ) - .then(() => { - setPreDisabled(false); - }) - .catch((err: ErrorResponseHandler) => { - dispatch( - setErrorSnackMessage({ - errorMessage: "Error enabling logging", - detailedError: err.detailedError, - }) - ); - }); - onCloseEnableAndRefresh(); - }} - onClose={() => setEnableDialogOpen(false)} - confirmationContent={ - - Logging will be enabled with default values - - } - /> - {edit && tenant !== null && logInfo != null && !disabled && ( - - )} - - -

Audit Log

-
- - { - const targetD = e.target; - const checked = targetD.checked; - if (checked) { - setEnableDialogOpen(true); - } else { - setDisableDialogOpen(true); - } - }} - indicatorLabels={["Enabled", "Disabled"]} - /> - -
- {!disabled && !loadingTenantLogs && ( - - - - - -

Configuration

-
- - { - setEdit(true); - }} - icon={} - color="primary" - variant={"contained"} - /> - -
-
- -
- - - {loadingTenant ? ( - - - - ) : ( - - {logInfo?.logCPURequest != null && ( - - - - - )} - {logInfo?.logMemRequest != null && ( - - - - - )} - {logInfo?.image != null && ( - - - - - )} - {logInfo?.diskCapacityGB != null && ( - - - - - )} - {logInfo?.serviceAccountName != null && ( - - - - - )} - {logInfo?.labels != null && logInfo.labels.length > 0 && ( - - - - - - - - - )} - {logInfo?.annotations != null && - logInfo.annotations.length > 0 && ( - - - - - - - - - )} - {logInfo?.nodeSelector != null && - logInfo.nodeSelector.length > 0 && ( - - - - - - - - - )} - - )} - -
- -
CPU Request:{logInfo?.logCPURequest}
Memory Request:{niceBytes(logInfo?.logMemRequest, true)}
Image:{logInfo?.image}
- Disk Capacity (GB): - {logInfo?.diskCapacityGB}
Service Account:{logInfo?.serviceAccountName}
-

Labels

-
- 0 - ? logInfo.labels - : [] - } - recordName="Labels" - /> -
-

Annotations

-
- 0 - ? logInfo.annotations - : [] - } - recordName="Annotations" - /> -
-

Node Selector

-
- 0 - ? logInfo.nodeSelector - : [] - } - recordName="Node Selector" - /> -
- -

Database Details

-
- - - {loadingTenant ? ( - - - - ) : ( - - {logInfo?.logDBCPURequest != null && ( - - - - - )} - {logInfo?.logDBMemRequest != null && ( - - - - - )} - {logInfo?.dbImage != null && ( - - - - - )} - {logInfo?.dbServiceAccountName != null && ( - - - - - )} - {logInfo?.dbLabels != null && - logInfo.dbLabels.length > 0 && ( - - - - - - - - - - )} - {logInfo?.annotations != null && - logInfo.dbAnnotations.length > 0 && ( - - - - - - - - - )} - {logInfo?.nodeSelector != null && - logInfo.dbNodeSelector.length > 0 && ( - - - - - - - - - )} - - )} - -
- -
DB CPU Request:{logInfo?.logDBCPURequest}
- DB Memory Request: - {niceBytes(logInfo?.logDBMemRequest, true)}
Postgres Image:{logInfo?.dbImage}
Service Account:{logInfo?.dbServiceAccountName}
-

Labels

-
- 0 - ? logInfo.dbLabels - : [] - } - recordName="labels" - /> -
-

Annotations

-
- 0 - ? logInfo.dbAnnotations - : [] - } - recordName="annotations" - /> -
-

Node Selector

-
- 0 - ? logInfo.dbNodeSelector - : [] - } - recordName="node selectors" - /> -
-
-
-
- )} -
- ); -}; - -const mapState = (state: AppState) => ({ - loadingTenant: state.tenants.loadingTenant, - selectedTenant: state.tenants.currentTenant, - tenant: state.tenants.tenantInfo, -}); - -const connector = connect(mapState, null); - -export default withStyles(styles)(connector(TenantLogging)); diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/tenantAuditLogSlice.ts b/portal-ui/src/screens/Console/Tenants/TenantDetails/tenantAuditLogSlice.ts new file mode 100644 index 000000000..7f8fcc230 --- /dev/null +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/tenantAuditLogSlice.ts @@ -0,0 +1,137 @@ +// 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 . +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { IKeyValue } from "../ListTenants/types"; + +export interface IEditTenantAuditLogging { + auditLoggingEnabled: boolean; + image: string; + labels: IKeyValue[]; + annotations: IKeyValue[]; + nodeSelector: IKeyValue[]; + diskCapacityGB: number; + serviceAccountName: string; + dbImage: string; + dbInitImage: string; + dbLabels: IKeyValue[]; + dbAnnotations: IKeyValue[]; + dbNodeSelector: IKeyValue[]; + dbServiceAccountName: string; + cpuRequest: string; + memRequest: string; + dbCPURequest: string; + dbMemRequest: string; +} + +const initialState: IEditTenantAuditLogging = { + auditLoggingEnabled: false, + image: "", + labels: [{ key: " ", value: " " }], + annotations: [{ key: " ", value: " " }], + nodeSelector: [{ key: " ", value: " " }], + diskCapacityGB: 0, + serviceAccountName: "", + dbCPURequest: "", + dbMemRequest: "", + dbImage: "", + dbInitImage: "", + dbLabels: [{ key: " ", value: " " }], + dbAnnotations: [{ key: " ", value: " " }], + dbNodeSelector: [{ key: " ", value: " " }], + dbServiceAccountName: "", + cpuRequest: "", + memRequest: "", +}; + +export const editTenantAuditLoggingSlice = createSlice({ + name: "editTenantAuditLogging", + initialState, + reducers: { + setAuditLoggingEnabled: (state, action: PayloadAction) => { + state.auditLoggingEnabled = action.payload; + }, + setImage: (state, action: PayloadAction) => { + state.image = action.payload; + }, + setDBImage: (state, action: PayloadAction) => { + state.dbImage = action.payload; + }, + setDBInitImage: (state, action: PayloadAction) => { + state.dbInitImage = action.payload; + }, + setLabels: (state, action: PayloadAction) => { + state.labels = action.payload; + }, + setAnnotations: (state, action: PayloadAction) => { + state.annotations = action.payload; + }, + setNodeSelector: (state, action: PayloadAction) => { + state.nodeSelector = action.payload; + }, + setDBLabels: (state, action: PayloadAction) => { + state.dbLabels = action.payload; + }, + setDBAnnotations: (state, action: PayloadAction) => { + state.dbAnnotations = action.payload; + }, + setDBNodeSelector: (state, action: PayloadAction) => { + state.dbNodeSelector = action.payload; + }, + setDiskCapacityGB: (state, action: PayloadAction) => { + state.diskCapacityGB = action.payload; + }, + setServiceAccountName: (state, action: PayloadAction) => { + state.serviceAccountName = action.payload; + }, + setDBServiceAccountName: (state, action: PayloadAction) => { + state.dbServiceAccountName = action.payload; + }, + setCPURequest: (state, action: PayloadAction) => { + state.cpuRequest = action.payload; + }, + setMemRequest: (state, action: PayloadAction) => { + state.memRequest = action.payload; + }, + setDBCPURequest: (state, action: PayloadAction) => { + state.dbCPURequest = action.payload; + }, + setDBMemRequest: (state, action: PayloadAction) => { + state.dbMemRequest = action.payload; + }, + }, +}); + +export const { + setAuditLoggingEnabled, + setImage, + setDBImage, + setDBInitImage, + setLabels, + setAnnotations, + setNodeSelector, + setDBLabels, + setDBAnnotations, + setDBNodeSelector, + setDiskCapacityGB, + setServiceAccountName, + setDBServiceAccountName, + setCPURequest, + setMemRequest, + setDBCPURequest, + setDBMemRequest, +} = editTenantAuditLoggingSlice.actions; + +export default editTenantAuditLoggingSlice.reducer; diff --git a/portal-ui/src/store.ts b/portal-ui/src/store.ts index 5d3f57d30..f2c82b81f 100644 --- a/portal-ui/src/store.ts +++ b/portal-ui/src/store.ts @@ -32,6 +32,7 @@ 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"; +import editTenantAuditLoggingReducer from "./screens/Console/Tenants/TenantDetails/tenantAuditLogSlice"; const rootReducer = combineReducers({ system: systemReducer, @@ -51,6 +52,7 @@ const rootReducer = combineReducers({ addPool: addPoolReducer, editPool: editPoolReducer, editTenantMonitoring: editTenantMonitoringReducer, + editTenantLogging: editTenantAuditLoggingReducer, }); export const store = configureStore({