Added identifier field to Event destinations page & migrated to mds (#2816)

This commit is contained in:
Alex
2023-05-16 20:21:31 -06:00
committed by GitHub
parent 58b64a5739
commit fc9319e55b
13 changed files with 539 additions and 461 deletions

View File

@@ -14,84 +14,23 @@
// 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 { Box } from "@mui/material";
import { HelpIconFilled, LambdaNotificationsIcon } from "mds";
const FeatureItem = ({
icon,
description,
}: {
icon: any;
description: string;
}) => {
return (
<Box
sx={{
display: "flex",
"& .min-icon": {
marginRight: "10px",
height: "23px",
width: "23px",
marginBottom: "10px",
},
}}
>
{icon}{" "}
<div style={{ fontSize: "14px", fontStyle: "italic", color: "#5E5E5E" }}>
{description}
</div>
</Box>
);
};
import { HelpBox, LambdaNotificationsIcon, Box } from "mds";
const NotificationEndpointTypeSelectorHelpBox = () => {
return (
<Box
sx={{
flex: 1,
border: "1px solid #eaeaea",
borderRadius: "2px",
display: "flex",
flexFlow: "column",
padding: "20px",
marginTop: {
xs: "0px",
},
}}
>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
display: "flex",
alignItems: "center",
marginBottom: "16px",
paddingBottom: "20px",
"& .min-icon": {
height: "21px",
width: "21px",
marginRight: "15px",
},
}}
>
<HelpIconFilled />
<div>Learn more about Event Destinations</div>
</Box>
<Box sx={{ fontSize: "14px", marginBottom: "15px" }}>
<Box sx={{ paddingBottom: "20px" }}>
<FeatureItem
icon={<LambdaNotificationsIcon />}
description={`What are Event Destinations?`}
/>
<Box sx={{ paddingTop: "20px" }}>
MinIO bucket notifications allow administrators to send
notifications to supported external services on certain object or
bucket events. MinIO supports bucket and object-level S3 events
similar to the Amazon S3 Event Notifications.
</Box>
<HelpBox
iconComponent={<LambdaNotificationsIcon />}
title={"What are Event Destinations?"}
help={
<Box sx={{ paddingTop: "20px" }}>
MinIO bucket notifications allow administrators to send notifications
to supported external services on certain object or bucket events.
MinIO supports bucket and object-level S3 events similar to the Amazon
S3 Event Notifications.
</Box>
</Box>
</Box>
}
/>
);
};

View File

@@ -400,7 +400,7 @@ export const typesSelection = {
height: "80px",
},
lambdaNotif: {
background: "#ffffff",
background: "#ffffff50",
border: "#E5E5E5 1px solid",
borderRadius: 5,
width: 250,

View File

@@ -17,19 +17,18 @@
import React, { Fragment, useCallback, useEffect, useState } from "react";
import get from "lodash/get";
import Grid from "@mui/material/Grid";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { BackLink, Button, PageLayout } from "mds";
import { BackLink, Button, FormLayout, Grid, InputBox, PageLayout } from "mds";
import api from "../../../common/api";
import {
destinationList,
notificationEndpointsFields,
notifyMysql,
notifyPostgres,
removeEmptyFields,
destinationList,
} from "./utils";
import {
modalBasic,
@@ -49,6 +48,8 @@ import {
import { useNavigate, useParams } from "react-router-dom";
import { useAppDispatch } from "../../../store";
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
import TargetTitle from "./TargetTitle";
import { setDestinationLoading } from "./destinationsSlice";
const ConfMySql = withSuspense(
React.lazy(() => import("./CustomForms/ConfMySql"))
@@ -66,43 +67,6 @@ const styles = (theme: Theme) =>
createStyles({
...modalBasic,
...settingsCommon,
lambdaNotif: {
background:
"linear-gradient(90deg, rgba(249,249,250,1) 0%, rgba(250,250,251,1) 68%, rgba(254,254,254,1) 100%)",
border: "#E5E5E5 1px solid",
borderRadius: 5,
height: 80,
display: "flex",
alignItems: "center",
justifyContent: "start",
marginBottom: 16,
cursor: "pointer",
padding: 0,
overflow: "hidden",
},
lambdaNotifIcon: {
backgroundColor: "#FEFEFE",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 80,
height: 80,
"& img": {
maxWidth: 46,
maxHeight: 46,
},
},
lambdaNotifTitle: {
color: "#07193E",
fontSize: 16,
fontFamily: "Inter,sans-serif",
paddingLeft: 18,
},
formBox: {
border: "1px solid #EAEAEA",
padding: 15,
},
});
interface IAddNotificationEndpointProps {
@@ -120,20 +84,22 @@ const AddEventDestination = ({
//Local States
const [valuesArr, setValueArr] = useState<IElementValue[]>([]);
const [identifier, setIdentifier] = useState<string>("");
const [saving, setSaving] = useState<boolean>(false);
const service = params.service || "";
//Effects
//Effects
useEffect(() => {
if (saving) {
const payload = {
key_values: removeEmptyFields(valuesArr),
};
api
.invoke("PUT", `/api/v1/configs/${service}`, payload)
.invoke("PUT", `/api/v1/configs/${service}:${identifier}`, payload)
.then(() => {
setSaving(false);
dispatch(setServerNeedsRestart(true));
dispatch(setDestinationLoading(true));
navigate(IAM_PAGES.EVENT_DESTINATIONS);
})
.catch((err: ErrorResponseHandler) => {
@@ -141,7 +107,15 @@ const AddEventDestination = ({
dispatch(setErrorSnackMessage(err));
});
}
}, [saving, service, valuesArr, saveAndRefresh, dispatch, navigate]);
}, [
saving,
service,
valuesArr,
saveAndRefresh,
dispatch,
navigate,
identifier,
]);
//Fetch Actions
const submitForm = (event: React.FormEvent) => {
@@ -199,41 +173,54 @@ const AddEventDestination = ({
<Fragment>
<Grid item xs={12}>
{targetElement && (
<div
key={`icon-${targetElement.targetTitle}`}
className={classes.lambdaNotif}
>
<div className={classes.lambdaNotifIcon}>
<img
src={targetElement.logo}
className={classes.logoButton}
alt={targetElement.targetTitle}
/>
</div>
<div className={classes.lambdaNotifTitle}>
<b>
{targetElement ? targetElement.targetTitle : ""} Event
Destination
</b>
</div>
</div>
<TargetTitle
logoSrc={targetElement.logo}
title={`${
targetElement ? targetElement.targetTitle : ""
} Event
Destination`}
/>
)}
</Grid>
<div className={classes.formBox}>
<Grid item xs={12} className={classes.configForm}>
<FormLayout>
<Grid
item
xs={12}
className={classes.formFieldRow}
sx={{ marginBottom: "12px" }}
>
<InputBox
id={"identifier-field"}
name={"identifier-field"}
label={"Identifier"}
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
tooltip={"Unique descriptive string for this destination"}
placeholder="Enter Destination Identifier"
required
/>
</Grid>
<Grid item xs={12}>
{srvComponent}
</Grid>
<Grid item xs={12} className={classes.settingsButtonContainer}>
<Grid
item
xs={12}
sx={{
display: "flex",
justifyContent: "flex-end",
marginTop: 15,
}}
>
<Button
id={"save-notification-target"}
type="submit"
variant="callAction"
disabled={saving}
disabled={saving || identifier.trim() === ""}
label={"Save Event Destination"}
/>
</Grid>
</div>
</FormLayout>
</Fragment>
)}
</form>

View File

@@ -24,12 +24,10 @@ import {
formFieldStyles,
modalBasic,
} from "../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import CSVMultiSelector from "../Common/FormComponents/CSVMultiSelector/CSVMultiSelector";
import CommentBoxWrapper from "../Common/FormComponents/CommentBoxWrapper/CommentBoxWrapper";
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import PredefinedList from "../Common/FormComponents/PredefinedList/PredefinedList";
import { ConsoleIcon, Tooltip } from "mds";
import { ConsoleIcon, InputBox, Switch, Tooltip } from "mds";
interface IConfGenericProps {
onChange: (newValue: IElementValue[]) => void;
@@ -144,7 +142,7 @@ const ConfTargetGeneric = ({
const value = holderItem ? holderItem.value : "off";
return (
<FormSwitchWrapper
<Switch
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.checked ? "on" : "off";
setValueElement(field.name, value, item);
@@ -195,7 +193,7 @@ const ConfTargetGeneric = ({
);
default:
return (
<InputBoxWrapper
<InputBox
id={field.name}
name={field.name}
label={field.label}
@@ -204,7 +202,6 @@ const ConfTargetGeneric = ({
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValueElement(field.name, e.target.value, item)
}
multiline={!!field.multiline}
placeholder={field.placeholder}
/>
);

View File

@@ -1,7 +1,22 @@
// 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 ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog";
import { ConfirmModalIcon } from "mds";
import { DialogContentText } from "@mui/material";
const ConfirmDeleteDestinationModal = ({
onConfirm,
@@ -25,11 +40,9 @@ const ConfirmDeleteDestinationModal = ({
onClose={onClose}
confirmationContent={
<React.Fragment>
<DialogContentText>
Are you sure you want to delete the event destination ?
<br />
<b>{serviceName}</b> which is <b>{status}</b>
</DialogContentText>
Are you sure you want to delete the event destination ?
<br />
<b>{serviceName}</b> which is <b>{status}</b>
</React.Fragment>
}
/>

View File

@@ -18,17 +18,13 @@ import React, { useCallback, useEffect, useState } from "react";
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 InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import { IElementValue } from "../../Configurations/types";
import {
formFieldStyles,
modalBasic,
} from "../../Common/FormComponents/common/styleLibrary";
import CommentBoxWrapper from "../../Common/FormComponents/CommentBoxWrapper/CommentBoxWrapper";
import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import PredefinedList from "../../Common/FormComponents/PredefinedList/PredefinedList";
import { Switch, InputBox, Grid, Box, ReadBox, RadioGroup } from "mds";
interface IConfMySqlProps {
onChange: (newValue: IElementValue[]) => void;
@@ -138,8 +134,8 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
return (
<Grid container>
<Grid item xs={12}>
<FormSwitchWrapper
<Grid item xs={12} className={classes.formFieldRow}>
<Switch
label={"Enter DNS String"}
checked={useDsnString}
id="checkedB"
@@ -151,7 +147,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
{useDsnString ? (
<React.Fragment>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="dsn-string"
name="dsn_string"
label="DSN String"
@@ -165,9 +161,17 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
) : (
<React.Fragment>
<Grid item xs={12}>
<Grid item xs={12} className={classes.configureString}>
<Box
withBorders
useBackground
sx={{
overflowY: "auto",
height: 170,
marginBottom: 12,
}}
>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="host"
name="host"
label=""
@@ -179,7 +183,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="db-name"
name="db-name"
label=""
@@ -191,7 +195,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="port"
name="port"
label=""
@@ -204,7 +208,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="user"
name="user"
label=""
@@ -216,7 +220,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="password"
name="password"
label=""
@@ -228,16 +232,17 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
}}
/>
</Grid>
</Grid>
</Box>
</Grid>
<PredefinedList label={"Connection String"} content={dsnString} />
<Grid item xs={12}>
<br />
<Grid item xs={12} sx={{ margin: "12px 0" }}>
<ReadBox label={"Connection String"} multiLine>
{dsnString}
</ReadBox>
</Grid>
</React.Fragment>
)}
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="table"
name="table"
label="Table"
@@ -250,8 +255,8 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<RadioGroupSelector
currentSelection={format}
<RadioGroup
currentValue={format}
id="format"
name="format"
label="Format"
@@ -266,7 +271,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="queue-dir"
name="queue_dir"
label="Queue Dir"
@@ -279,7 +284,7 @@ const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="queue-limit"
name="queue_limit"
label="Queue Limit"

View File

@@ -18,18 +18,13 @@ import React, { useCallback, useEffect, useState } from "react";
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 InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { Box, Grid, InputBox, RadioGroup, ReadBox, Select, Switch } from "mds";
import { IElementValue } from "../../Configurations/types";
import {
formFieldStyles,
modalBasic,
} from "../../Common/FormComponents/common/styleLibrary";
import CommentBoxWrapper from "../../Common/FormComponents/CommentBoxWrapper/CommentBoxWrapper";
import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import PredefinedList from "../../Common/FormComponents/PredefinedList/PredefinedList";
interface IConfPostgresProps {
onChange: (newValue: IElementValue[]) => void;
@@ -205,8 +200,8 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
return (
<Grid container>
<Grid item xs={12}>
<FormSwitchWrapper
<Grid item xs={12} className={classes.formFieldRow}>
<Switch
label={"Manually Configure String"}
checked={useConnectionString}
id="manualString"
@@ -220,7 +215,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
{useConnectionString ? (
<React.Fragment>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="connection-string"
name="connection_string"
label="Connection String"
@@ -234,9 +229,17 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
) : (
<React.Fragment>
<Grid item xs={12}>
<Grid item xs={12} className={classes.configureString}>
<Box
withBorders
useBackground
sx={{
overflowY: "auto",
height: 170,
marginBottom: 12,
}}
>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="host"
name="host"
label=""
@@ -248,7 +251,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="db-name"
name="db-name"
label=""
@@ -260,7 +263,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="port"
name="port"
label=""
@@ -272,14 +275,14 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<SelectWrapper
<Select
value={sslMode}
label=""
id="sslmode"
name="sslmode"
onChange={(e): void => {
if (e.target.value !== undefined) {
setSslMode(e.target.value + "");
onChange={(value): void => {
if (value) {
setSslMode(value + "");
}
}}
options={[
@@ -292,7 +295,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="user"
name="user"
label=""
@@ -304,7 +307,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="password"
name="password"
label=""
@@ -316,19 +319,17 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
}}
/>
</Grid>
</Grid>
</Box>
</Grid>
<PredefinedList
label={"Connection String"}
content={connectionString}
/>
<Grid item xs={12}>
<br />
<Grid item xs={12} className={classes.formFieldRow}>
<ReadBox label={"Connection String"} multiLine>
{connectionString}
</ReadBox>
</Grid>
</React.Fragment>
)}
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="table"
name="table"
label="Table"
@@ -341,8 +342,8 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<RadioGroupSelector
currentSelection={format}
<RadioGroup
currentValue={format}
id="format"
name="format"
label="Format"
@@ -357,7 +358,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="queue-dir"
name="queue_dir"
label="Queue Dir"
@@ -370,7 +371,7 @@ const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
<InputBox
id="queue-limit"
name="queue_limit"
label="Queue Limit"

View File

@@ -0,0 +1,83 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 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 get from "lodash/get";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
interface IDestinationButton {
destinationType: string;
srcImage: string;
title: string;
}
const DestinationButtonBase = styled.button(({ theme }) => ({
background: get(theme, "boxBackground", "#FFF"),
border: `${get(theme, "borderColor", "#E2E2E2")} 1px solid`,
borderRadius: 5,
width: 250,
height: 80,
display: "flex",
alignItems: "center",
justifyContent: "start",
marginBottom: 16,
marginRight: 8,
cursor: "pointer",
overflow: "hidden",
"&:hover": {
backgroundColor: get(theme, "buttons.regular.hover.background", "#ebebeb"),
},
"& .imageContainer": {
width: 80,
"& .logoButton": {
maxWidth: 46,
maxHeight: 46,
filter: "drop-shadow(1px 1px 8px #fff)",
},
},
"& .lambdaNotifTitle": {
color: get(theme, "buttons.callAction.enabled.background", "#07193E"),
fontSize: 16,
fontFamily: "Inter,sans-serif",
paddingLeft: 18,
fontWeight: "bold",
},
}));
const DestinationButton = ({
destinationType,
srcImage,
title,
}: IDestinationButton) => {
const navigate = useNavigate();
return (
<DestinationButtonBase
onClick={() => {
navigate(`${IAM_PAGES.EVENT_DESTINATIONS_ADD}/${destinationType}`);
}}
>
<span className={"imageContainer"}>
<img src={srcImage} className={"logoButton"} alt={title} />
</span>
<span className={"lambdaNotifTitle"}>{title}</span>
</DestinationButtonBase>
);
};
export default DestinationButton;

View File

@@ -15,24 +15,14 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment } from "react";
import { Theme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { BackLink, Box, FormLayout, PageLayout } from "mds";
import { destinationList, DestType } from "./utils";
import {
settingsCommon,
typesSelection,
} from "../Common/FormComponents/common/styleLibrary";
import { typesSelection } from "../Common/FormComponents/common/styleLibrary";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import { Box } from "@mui/material";
import NotificationEndpointTypeSelectorHelpBox from "../Account/NotificationEndpointTypeSelectorHelpBox";
import { BackLink, PageLayout } from "mds";
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
interface INotificationTypeSelector {
classes: any;
}
import DestinationButton from "./DestinationButton";
const withLogos = destinationList.filter((elService) => elService.logo !== "");
const database = withLogos.filter(
@@ -45,13 +35,7 @@ const functions = withLogos.filter(
(elService) => elService.category === DestType.Func
);
const styles = (theme: Theme) =>
createStyles({
...settingsCommon,
...typesSelection,
});
const EventTypeSelector = ({ classes }: INotificationTypeSelector) => {
const EventTypeSelector = () => {
const navigate = useNavigate();
return (
<Fragment>
@@ -67,115 +51,58 @@ const EventTypeSelector = ({ classes }: INotificationTypeSelector) => {
actions={<React.Fragment />}
/>
<PageLayout>
<Box
sx={{
display: "grid",
padding: "16px",
gap: "8px",
gridTemplateColumns: {
md: "2fr 1.2fr",
xs: "1fr",
},
border: "1px solid #eaeaea",
}}
>
<div>
<div style={{ fontSize: 16, fontWeight: 600, paddingBottom: 15 }}>
<FormLayout helpBox={<NotificationEndpointTypeSelectorHelpBox />}>
<Box>
<Box sx={{ fontSize: 16, fontWeight: 600, paddingBottom: 15 }}>
Queue
</div>
<div className={classes.iconContainer}>
</Box>
<Box sx={{ ...typesSelection.iconContainer }}>
{queue.map((item) => {
return (
<button
<DestinationButton
destinationType={item.actionTrigger}
srcImage={item.logo}
title={item.targetTitle}
key={`icon-${item.targetTitle}`}
className={classes.lambdaNotif}
onClick={() => {
navigate(
`${IAM_PAGES.EVENT_DESTINATIONS_ADD}/${item.actionTrigger}`
);
}}
>
<div className={classes.lambdaNotifIcon}>
<img
src={item.logo}
className={classes.logoButton}
alt={item.targetTitle}
/>
</div>
<div className={classes.lambdaNotifTitle}>
<b>{item.targetTitle}</b>
</div>
</button>
/>
);
})}
</div>
<div style={{ fontSize: 16, fontWeight: 600, paddingBottom: 15 }}>
</Box>
<Box sx={{ fontSize: 16, fontWeight: 600, paddingBottom: 15 }}>
Database
</div>
<div className={classes.iconContainer}>
</Box>
<Box sx={{ ...typesSelection.iconContainer }}>
{database.map((item) => {
return (
<button
<DestinationButton
destinationType={item.actionTrigger}
srcImage={item.logo}
title={item.targetTitle}
key={`icon-${item.targetTitle}`}
className={classes.lambdaNotif}
onClick={() => {
navigate(
`${IAM_PAGES.EVENT_DESTINATIONS_ADD}/${item.actionTrigger}`
);
}}
>
<div className={classes.lambdaNotifIcon}>
<img
src={item.logo}
className={classes.logoButton}
alt={item.targetTitle}
/>
</div>
<div className={classes.lambdaNotifTitle}>
<b>{item.targetTitle}</b>
</div>
</button>
/>
);
})}
</div>
<div style={{ fontSize: 16, fontWeight: 600, paddingBottom: 15 }}>
</Box>
<Box sx={{ fontSize: 16, fontWeight: 600, paddingBottom: 15 }}>
Functions
</div>
<div className={classes.iconContainer}>
</Box>
<Box sx={{ ...typesSelection.iconContainer }}>
{functions.map((item) => {
return (
<button
<DestinationButton
destinationType={item.actionTrigger}
srcImage={item.logo}
title={item.targetTitle}
key={`icon-${item.targetTitle}`}
className={classes.lambdaNotif}
onClick={() => {
navigate(
`${IAM_PAGES.EVENT_DESTINATIONS_ADD}/${item.actionTrigger}`
);
}}
>
<div className={classes.lambdaNotifIcon}>
<img
src={item.logo}
className={classes.logoButton}
alt={item.targetTitle}
/>
</div>
<div className={classes.lambdaNotifTitle}>
<b>{item.targetTitle}</b>
</div>
</button>
/>
);
})}
</div>
</div>
<NotificationEndpointTypeSelectorHelpBox />
</Box>
</Box>
</Box>
</FormLayout>
</PageLayout>
</Fragment>
);
};
export default withStyles(styles)(EventTypeSelector);
export default EventTypeSelector;

View File

@@ -19,6 +19,8 @@ import {
AddIcon,
Box,
Button,
DataTable,
Grid,
HelpBox,
LambdaIcon,
PageLayout,
@@ -30,7 +32,6 @@ import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { LinearProgress } from "@mui/material";
import { red } from "@mui/material/colors";
import Grid from "@mui/material/Grid";
import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
import {
NotificationEndpointItem,
@@ -38,7 +39,6 @@ import {
TransformedEndpointItem,
} from "./types";
import { getNotificationConfigKey, notificationTransform } from "./utils";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import {
actionsTray,
@@ -57,9 +57,11 @@ import {
setErrorSnackMessage,
setServerNeedsRestart,
} from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
import { AppState, useAppDispatch } from "../../../store";
import ConfirmDeleteDestinationModal from "./ConfirmDeleteDestinationModal";
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
import { useSelector } from "react-redux";
import { setDestinationLoading } from "./destinationsSlice";
interface IListNotificationEndpoints {
classes: any;
@@ -70,9 +72,6 @@ const styles = (theme: Theme) =>
...actionsTray,
...settingsCommon,
...containerForHeader,
twHeight: {
minHeight: 400,
},
tableBlock: {
...tableStyles.tableBlock,
},
@@ -92,10 +91,12 @@ const styles = (theme: Theme) =>
const ListEventDestinations = ({ classes }: IListNotificationEndpoints) => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
// Reducer States
const isLoading = useSelector((state: AppState) => state.destination.loading);
//Local States
const [records, setRecords] = useState<TransformedEndpointItem[]>([]);
const [filter, setFilter] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isDelConfirmOpen, setIsDelConfirmOpen] = useState<boolean>(false);
const [selNotifyEndPoint, setSelNotifyEndpoint] =
@@ -114,11 +115,11 @@ const ListEventDestinations = ({ classes }: IListNotificationEndpoints) => {
resNotEndList = res.notification_endpoints;
}
setRecords(notificationTransform(resNotEndList));
setIsLoading(false);
dispatch(setDestinationLoading(false));
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setIsLoading(false);
dispatch(setDestinationLoading(false));
});
};
fetchRecords();
@@ -126,8 +127,8 @@ const ListEventDestinations = ({ classes }: IListNotificationEndpoints) => {
}, [isLoading, dispatch]);
useEffect(() => {
setIsLoading(true);
}, []);
dispatch(setDestinationLoading(true));
}, [dispatch]);
const resetNotificationConfig = (
ep: TransformedEndpointItem | undefined | null
@@ -142,6 +143,7 @@ const ListEventDestinations = ({ classes }: IListNotificationEndpoints) => {
dispatch(setServerNeedsRestart(true));
setSelNotifyEndpoint(null);
setIsDelConfirmOpen(false);
dispatch(setDestinationLoading(true));
})
.catch((err: ErrorResponseHandler) => {
setIsDelConfirmOpen(false);
@@ -188,142 +190,146 @@ const ListEventDestinations = ({ classes }: IListNotificationEndpoints) => {
return (
<Fragment>
<PageLayout>
<Grid item xs={12} className={classes.actionsTray}>
<SearchBox
placeholder="Search target"
onChange={setFilter}
overrideClass={classes.searchField}
value={filter}
/>
<div className={classes.rightActionItems}>
<TooltipWrapper tooltip={"Refresh List"}>
<Button
id={"reload-event-destinations"}
label={"Refresh"}
variant="regular"
icon={<RefreshIcon />}
onClick={() => {
setIsLoading(true);
}}
/>
</TooltipWrapper>
<TooltipWrapper tooltip={"Add Event Destination"}>
<Button
id={"add-notification-target"}
label={"Add Event Destination"}
variant="callAction"
icon={<AddIcon />}
onClick={() => {
navigate(IAM_PAGES.EVENT_DESTINATIONS_ADD);
}}
/>
</TooltipWrapper>
</div>
</Grid>
{isLoading && <LinearProgress />}
{!isLoading && (
<Fragment>
{records.length > 0 && (
<Fragment>
<Grid item xs={12} className={classes.tableBlock}>
<Box sx={{ width: "100%" }}>
<TableWrapper
itemActions={tableActions}
columns={[
{
label: "Status",
elementKey: "status",
renderFunction: statusDisplay,
width: 150,
},
{ label: "Service", elementKey: "service_name" },
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Event Destinations"
idField="service_name"
customPaperHeight={classes.twHeight}
<Grid container sx={{ width: "100%" }}>
<Grid item xs={12} className={classes.actionsTray}>
<SearchBox
placeholder="Search target"
onChange={setFilter}
overrideClass={classes.searchField}
value={filter}
/>
<div className={classes.rightActionItems}>
<TooltipWrapper tooltip={"Refresh List"}>
<Button
id={"reload-event-destinations"}
label={"Refresh"}
variant="regular"
icon={<RefreshIcon />}
onClick={() => {
dispatch(setDestinationLoading(true));
}}
/>
</TooltipWrapper>
<TooltipWrapper tooltip={"Add Event Destination"}>
<Button
id={"add-notification-target"}
label={"Add Event Destination"}
variant="callAction"
icon={<AddIcon />}
onClick={() => {
navigate(IAM_PAGES.EVENT_DESTINATIONS_ADD);
}}
/>
</TooltipWrapper>
</div>
</Grid>
{isLoading && <LinearProgress />}
{!isLoading && (
<Fragment>
{records.length > 0 && (
<Fragment>
<Grid item xs={12} className={classes.tableBlock}>
<Box sx={{ width: "100%" }}>
<DataTable
itemActions={tableActions}
columns={[
{
label: "Status",
elementKey: "status",
renderFunction: statusDisplay,
width: 150,
},
{ label: "Service", elementKey: "service_name" },
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Event Destinations"
idField="service_name"
customPaperHeight={"400px"}
/>
</Box>
</Grid>
<Grid item xs={12} sx={{ marginTop: 15 }}>
<HelpBox
title={"Event Destinations"}
iconComponent={<LambdaIcon />}
help={
<Fragment>
MinIO bucket notifications allow administrators to
send notifications to supported external services on
certain object or bucket events. MinIO supports bucket
and object-level S3 events similar to the Amazon S3
Event Notifications.
<br />
<br />
You can learn more at our{" "}
<a
href="https://min.io/docs/minio/linux/administration/monitoring/bucket-notifications.html?ref=con"
target="_blank"
rel="noopener"
>
documentation
</a>
.
</Fragment>
}
/>
</Box>
</Grid>
</Fragment>
)}
{records.length === 0 && (
<Grid
container
sx={{
justifyContent: "center",
alignContent: "center",
alignItems: "center",
}}
>
<Grid item xs={8}>
<HelpBox
title={"Event Destinations"}
iconComponent={<LambdaIcon />}
help={
<Fragment>
MinIO bucket notifications allow administrators to
send notifications to supported external services on
certain object or bucket events. MinIO supports bucket
and object-level S3 events similar to the Amazon S3
Event Notifications.
<br />
<br />
To get started,{" "}
<AButton
onClick={() => {
navigate(IAM_PAGES.EVENT_DESTINATIONS_ADD);
}}
>
Add an Event Destination
</AButton>
.
</Fragment>
}
/>
</Grid>
</Grid>
<Grid item xs={12}>
<HelpBox
title={"Event Destinations"}
iconComponent={<LambdaIcon />}
help={
<Fragment>
MinIO bucket notifications allow administrators to send
notifications to supported external services on certain
object or bucket events. MinIO supports bucket and
object-level S3 events similar to the Amazon S3 Event
Notifications.
<br />
<br />
You can learn more at our{" "}
<a
href="https://min.io/docs/minio/linux/administration/monitoring/bucket-notifications.html?ref=con"
target="_blank"
rel="noopener"
>
documentation
</a>
.
</Fragment>
}
/>
</Grid>
</Fragment>
)}
{records.length === 0 && (
<Grid
container
justifyContent={"center"}
alignContent={"center"}
alignItems={"center"}
>
<Grid item xs={8}>
<HelpBox
title={"Event Destinations"}
iconComponent={<LambdaIcon />}
help={
<Fragment>
MinIO bucket notifications allow administrators to send
notifications to supported external services on certain
object or bucket events. MinIO supports bucket and
object-level S3 events similar to the Amazon S3 Event
Notifications.
<br />
<br />
To get started,{" "}
<AButton
onClick={() => {
navigate(IAM_PAGES.EVENT_DESTINATIONS_ADD);
}}
>
Add an Event Destination
</AButton>
.
</Fragment>
}
/>
</Grid>
</Grid>
)}
</Fragment>
)}
)}
</Fragment>
)}
{isDelConfirmOpen ? (
<ConfirmDeleteDestinationModal
onConfirm={() => {
resetNotificationConfig(selNotifyEndPoint);
}}
status={`${selNotifyEndPoint?.status}`}
serviceName={`${selNotifyEndPoint?.service_name}`}
onClose={() => {
setIsDelConfirmOpen(false);
}}
/>
) : null}
{isDelConfirmOpen ? (
<ConfirmDeleteDestinationModal
onConfirm={() => {
resetNotificationConfig(selNotifyEndPoint);
}}
status={`${selNotifyEndPoint?.status}`}
serviceName={`${selNotifyEndPoint?.service_name}`}
onClose={() => {
setIsDelConfirmOpen(false);
}}
/>
) : null}
</Grid>
</PageLayout>
</Fragment>
);

View File

@@ -0,0 +1,78 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 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 get from "lodash/get";
import styled from "styled-components";
import { Box } from "mds";
interface ITargetTitle {
logoSrc: string;
title: string;
}
const TargetBase = styled.div(({ theme }) => ({
background: get(theme, "boxBackground", "#fff"),
border: `${get(theme, "borderColor", "#E5E5E5")} 1px solid`,
borderRadius: 5,
height: 80,
display: "flex",
alignItems: "center",
justifyContent: "start",
marginBottom: 16,
cursor: "pointer",
padding: 0,
overflow: "hidden",
"& .logoButton": {
height: "80px",
},
"& .imageContainer": {
backgroundColor: get(theme, "bgColor", "#fff"),
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 80,
height: 80,
"& img": {
maxWidth: 46,
maxHeight: 46,
filter: "drop-shadow(1px 1px 8px #fff)",
},
},
"& .titleBox": {
color: get(theme, "fontColor", "#000"),
fontSize: 16,
fontFamily: "Inter,sans-serif",
paddingLeft: 18,
},
}));
const TargetTitle = ({ logoSrc, title }: ITargetTitle) => {
return (
<TargetBase>
<Box className={"imageContainer"}>
<img src={logoSrc} className={"logoButton"} alt={title} />
</Box>
<Box className={"titleBox"}>
<b>{title} Event Destination</b>
</Box>
</TargetBase>
);
};
export default TargetTitle;

View File

@@ -0,0 +1,40 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 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";
export interface DestinationState {
loading: boolean;
}
const initialState: DestinationState = {
loading: true,
};
export const destinationSlice = createSlice({
name: "destination",
initialState,
reducers: {
setDestinationLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
},
});
// Action creators are generated for each case reducer function
export const { setDestinationLoading } = destinationSlice.actions;
export default destinationSlice.reducer;

View File

@@ -30,6 +30,7 @@ import dashboardReducer from "./screens/Console/Dashboard/dashboardSlice";
import createUserReducer from "./screens/Console/Users/AddUsersSlice";
import licenseReducer from "./screens/Console/License/licenseSlice";
import registerReducer from "./screens/Console/Support/registerSlice";
import destinationSlice from "./screens/Console/EventDestinations/destinationsSlice";
const rootReducer = combineReducers({
system: systemReducer,
@@ -46,6 +47,7 @@ const rootReducer = combineReducers({
register: registerReducer,
createUser: createUserReducer,
license: licenseReducer,
destination: destinationSlice,
});
export const store = configureStore({